diff --git a/file-barj-core/build.gradle.kts b/file-barj-core/build.gradle.kts index 760c03d..cb68338 100644 --- a/file-barj-core/build.gradle.kts +++ b/file-barj-core/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { compileOnly(libs.jetbrains.annotations) implementation(project(":file-barj-stream-io")) implementation(libs.slf4j.api) + implementation(libs.bundles.validation) implementation(libs.bundles.jackson) implementation(libs.commons.compress) implementation(libs.commons.io) @@ -43,6 +44,7 @@ licensee { allow("MIT") allow("Apache-2.0") allow("BSD-2-Clause") + allow("GPL-2.0-with-classpath-exception") allowUrl("https://www.bouncycastle.org/licence.html") } diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/BaseFileMetadataChangeDetector.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/BaseFileMetadataChangeDetector.java index 7715c9b..f952127 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/BaseFileMetadataChangeDetector.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/BaseFileMetadataChangeDetector.java @@ -57,7 +57,6 @@ public boolean isFromLastIncrement( return filesFromManifests.get(filesFromManifests.lastKey()).containsKey(fileMetadata.getId()); } - @SuppressWarnings("checkstyle:TodoComment") @Nullable @Override public FileMetadata findMostRelevantPreviousVersion( diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImpl.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImpl.java index a0f07ca..0ecdfdf 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImpl.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImpl.java @@ -9,6 +9,10 @@ import com.github.nagyesta.filebarj.core.model.enums.OperatingSystem; import com.github.nagyesta.filebarj.core.util.LogUtil; import com.github.nagyesta.filebarj.core.util.OsUtil; +import jakarta.validation.Validation; +import jakarta.validation.ValidationException; +import jakarta.validation.Validator; +import jakarta.validation.groups.Default; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -36,6 +40,7 @@ public class ManifestManagerImpl implements ManifestManager { private static final String HISTORY_FOLDER = ".history"; private static final String MANIFEST_JSON_GZ = ".manifest.json.gz"; private final ObjectMapper mapper = new ObjectMapper(); + private final Validator validator = createValidator(); @Override public BackupIncrementManifest generateManifest( @@ -57,6 +62,7 @@ public BackupIncrementManifest generateManifest( .build(); Optional.ofNullable(jobConfiguration.getEncryptionKey()) .ifPresent(manifest::generateDataEncryptionKeys); + validate(manifest, ValidationRules.Created.class); return manifest; } @@ -197,12 +203,19 @@ public RestoreManifest mergeForRestore( .build(); } - @SuppressWarnings("checkstyle:TodoComment") @Override public void validate( @NonNull final BackupIncrementManifest manifest, @NonNull final Class forAction) { - //TODO: implement validation + + final var violations = validator.validate(manifest, forAction, Default.class); + if (!violations.isEmpty()) { + final var violationsMessage = violations.stream() + .map(v -> v.getPropertyPath().toString() + ": " + v.getMessage() + " (found: " + v.getInvalidValue() + ")") + .collect(Collectors.joining("\n\t")); + log.error("Manifest validation failed for {} action:\n\t{}", forAction.getSimpleName(), violationsMessage); + throw new ValidationException("The manifest is invalid!"); + } } @Override @@ -214,6 +227,12 @@ public void deleteIncrement( deleteManifestAndArchiveFilesFromBackupDirectory(backupDirectory, fileNamePrefix); } + private static Validator createValidator() { + try (var validatorFactory = Validation.buildDefaultValidatorFactory()) { + return validatorFactory.getValidator(); + } + } + @NotNull private SortedMap loadManifests( @NotNull final List manifestFiles, diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java index 859c14b..be9b467 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java @@ -9,6 +9,10 @@ import com.github.nagyesta.filebarj.core.json.PublicKeyDeserializer; import com.github.nagyesta.filebarj.core.json.PublicKeySerializer; import com.github.nagyesta.filebarj.core.model.enums.BackupType; +import com.github.nagyesta.filebarj.core.validation.FileNamePrefix; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; @@ -26,9 +30,7 @@ @EqualsAndHashCode @Builder @Jacksonized -@SuppressWarnings("checkstyle:TodoComment") public class BackupJobConfiguration { - //TODO: validate the whole configuration private static final int ONE_HUNDRED_GIBIBYTE = 100 * 1024; /** * The desired backup type which should be used when the job is executed. @@ -87,6 +89,7 @@ public class BackupJobConfiguration { *

* NOTE: Using 0 means that the archive won't be chunked. */ + @Positive @Builder.Default @EqualsAndHashCode.Exclude @JsonProperty("chunk_size_mebibyte") @@ -98,6 +101,7 @@ public class BackupJobConfiguration { * increments cannot use a different duplicate handling strategy. */ @NonNull + @FileNamePrefix @JsonProperty("file_name_prefix") private final String fileNamePrefix; /** @@ -112,6 +116,8 @@ public class BackupJobConfiguration { /** * The source files we want to archive. */ + @Valid + @Size(min = 1) @NonNull @EqualsAndHashCode.Exclude @JsonProperty("sources") diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java index 65de76c..1625d2c 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.nagyesta.filebarj.core.model.BackupPath; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Data; import lombok.NonNull; @@ -34,6 +37,7 @@ public class BackupSource { /** * The path we want to back up. Can be file or directory. */ + @Valid @NonNull @JsonProperty("path") private final BackupPath path; @@ -42,13 +46,13 @@ public class BackupSource { * with "glob" syntax relative to the value of the path field. */ @JsonProperty("include_patterns") - private final Set includePatterns; + private final Set<@NotNull @NotBlank String> includePatterns; /** * Optional exclude patterns for filtering the contents. Uses {@link java.nio.file.PathMatcher} * with "glob" syntax relative to the value of the path field. */ @JsonProperty("exclude_patterns") - private final Set excludePatterns; + private final Set<@NotNull @NotBlank String> excludePatterns; /** * Lists the matching {@link Path} entries. diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/AppVersion.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/AppVersion.java index 442bbf7..4bddb0e 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/AppVersion.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/AppVersion.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.validation.constraints.PositiveOrZero; import lombok.NonNull; import org.jetbrains.annotations.NotNull; @@ -18,7 +19,8 @@ * @param minor Minor version component * @param patch Patch version component */ -public record AppVersion(int major, int minor, int patch) implements Comparable { +public record AppVersion( + @PositiveOrZero int major, @PositiveOrZero int minor, @PositiveOrZero int patch) implements Comparable { /** * The version of the currently used file-barj component. diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java index f893414..550c5b9 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.PositiveOrZero; import lombok.Builder; import lombok.Data; import lombok.NonNull; @@ -27,6 +28,7 @@ public final class ArchiveEntryLocator { /** * The backup increment containing the entry. */ + @PositiveOrZero @JsonProperty("backup_increment") private final int backupIncrement; /** diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java index 7ed340d..be5d5ac 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Data; import lombok.NonNull; @@ -29,6 +31,7 @@ public class ArchivedFileMetadata { /** * The location where the archived file contents are stored. */ + @Valid @NonNull @JsonProperty("archive_location") private final ArchiveEntryLocator archiveLocation; @@ -46,6 +49,7 @@ public class ArchivedFileMetadata { * The Ids of the original files which are archived by the current entry. If multiple Ids are * listed, then duplicates where eliminated. */ + @Size(min = 1) @NonNull @JsonProperty("files") private Set files; diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java index a8b06af..e02f37e 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java @@ -3,6 +3,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.nagyesta.filebarj.core.config.BackupJobConfiguration; import com.github.nagyesta.filebarj.core.model.enums.BackupType; +import com.github.nagyesta.filebarj.core.validation.FileNamePrefix; +import com.github.nagyesta.filebarj.core.validation.PastOrPresentEpochSeconds; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; @@ -27,6 +31,7 @@ public class BackupIncrementManifest extends EncryptionKeyStore { /** * The version number of the app that generated the manifest. */ + @Valid @NonNull @JsonProperty("app_version") private AppVersion appVersion; @@ -34,12 +39,15 @@ public class BackupIncrementManifest extends EncryptionKeyStore { * The time when the backup process was started in UTC epoch * seconds. */ + @Positive + @PastOrPresentEpochSeconds @JsonProperty("start_time_utc_epoch_seconds") private long startTimeUtcEpochSeconds; /** * The file name prefix used by the backup archives. */ @NonNull + @FileNamePrefix @JsonProperty("file_name_prefix") private String fileNamePrefix; /** @@ -51,32 +59,46 @@ public class BackupIncrementManifest extends EncryptionKeyStore { /** * The OS of the backup. */ + @NotNull(groups = ValidationRules.Created.class) + @NotBlank(groups = ValidationRules.Created.class) @JsonProperty("operating_system") private String operatingSystem; /** * The snapshot of the backup configuration at the time of backup. */ + @Valid @NonNull @JsonProperty("job_configuration") private BackupJobConfiguration configuration; /** * The map of matching files identified during backup keyed by Id. */ + @Valid + @Size(max = 0, groups = ValidationRules.Created.class) + @Size(min = 1, groups = ValidationRules.Persisted.class) @JsonProperty("files") private Map files; /** * The map of archive entries saved during backup keyed by Id. */ + @Valid + @Size(max = 0, groups = ValidationRules.Created.class) @JsonProperty("archive_entries") private Map archivedEntries; /** * The name of the index file. */ + @Null(groups = ValidationRules.Created.class) + @NotNull(groups = ValidationRules.Persisted.class) + @NotBlank(groups = ValidationRules.Persisted.class) @JsonProperty("index_file_name") private String indexFileName; /** * The names of the data files. */ + @Null(groups = ValidationRules.Created.class) + @NotNull(groups = ValidationRules.Persisted.class) + @Size(min = 1, groups = ValidationRules.Persisted.class) @JsonProperty("data_file_names") private List dataFileNames; } diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupPath.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupPath.java index 4679bd9..1d1d928 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupPath.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupPath.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.validation.constraints.NotBlank; import lombok.EqualsAndHashCode; import lombok.NonNull; import org.apache.commons.io.FilenameUtils; @@ -31,6 +32,7 @@ public final class BackupPath implements Comparable { private static final Pattern UNIX_FILE_SCHEME = Pattern .compile("^" + FILE_SCHEME_DOUBLE_SLASH + "(?<" + PATH_GROUP + ">/[^:]*)$"); private static final Set PATTERNS = Set.of(WINDOWS_FILE_SCHEME, UNIX_FILE_SCHEME); + @NotBlank private final String path; /** diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/EncryptionKeyStore.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/EncryptionKeyStore.java index 9d7d702..12bc1be 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/EncryptionKeyStore.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/EncryptionKeyStore.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.nagyesta.filebarj.io.stream.crypto.EncryptionUtil; +import jakarta.validation.Valid; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; import lombok.*; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @@ -41,9 +44,11 @@ public class EncryptionKeyStore { * by 1. A manifest can contain more numbers if the backup increments were merged (consolidated) * into a single archive. */ + @Valid + @Size(min = 1) @NonNull @JsonProperty("backup_versions") - private SortedSet versions; + private SortedSet<@PositiveOrZero Integer> versions; /** * The byte arrays containing the data encryption keys (DEK) encrypted with the key encryption * key (KEK). diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java index eb7d46f..fba8896 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java @@ -5,6 +5,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.github.nagyesta.filebarj.core.model.enums.Change; import com.github.nagyesta.filebarj.core.model.enums.FileType; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.PositiveOrZero; import lombok.Builder; import lombok.Data; import lombok.NonNull; @@ -36,6 +41,7 @@ public class FileMetadata implements Comparable { /** * The absolute path where the file is located. */ + @Valid @NonNull @JsonProperty("path") private final BackupPath absolutePath; @@ -50,36 +56,48 @@ public class FileMetadata implements Comparable { /** * The original file size. */ + @NotNull + @PositiveOrZero @JsonProperty("original_size") private Long originalSizeBytes; /** * The last modified time of the file using UTC epoch seconds. */ + @NotNull @JsonProperty("last_modified_utc_epoch_seconds") private Long lastModifiedUtcEpochSeconds; /** * The last access time of the file using UTC epoch seconds. */ + @NotNull @JsonProperty("last_accessed_utc_epoch_seconds") private Long lastAccessedUtcEpochSeconds; /** * The creation time of the file using UTC epoch seconds. */ + @NotNull @JsonProperty("created_utc_epoch_seconds") private Long createdUtcEpochSeconds; /** * The POSIX permissions of the file. */ + @NotNull + @NotBlank + @Pattern(regexp = "^([r-][w-][x-]){3}$") @JsonProperty("permissions") private final String posixPermissions; /** * The owner of the file. */ + @NotNull + @NotBlank @JsonProperty("owner") private final String owner; /** * The owner group of the file. */ + @NotNull + @NotBlank @JsonProperty("group") private final String group; /** @@ -91,6 +109,7 @@ public class FileMetadata implements Comparable { /** * The hidden status of the file. */ + @NotNull @JsonProperty("hidden") private Boolean hidden; /** diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ValidationRules.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ValidationRules.java index 7301810..57d7bc1 100644 --- a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ValidationRules.java +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ValidationRules.java @@ -19,10 +19,4 @@ interface Created extends ValidationRules { interface Persisted extends ValidationRules { } - /** - * Defines the rule set used for configurations and related entities as they are expected to be - * ready for the restore to start. - */ - interface ReadyForRestore extends ValidationRules { - } } diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefix.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefix.java new file mode 100644 index 0000000..3492dce --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefix.java @@ -0,0 +1,35 @@ +package com.github.nagyesta.filebarj.core.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +import java.lang.annotation.*; + +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = FileNamePrefixValidator.class) +public @interface FileNamePrefix { + + + /** + * @return the error message template + */ + String message() default "Invalid file name prefix."; + + /** + * @return the groups the constraint belongs to + */ + Class[] groups() default {}; + + /** + * @return the payload associated to the constraint + */ + Class[] payload() default {}; + + Pattern pattern() default @Pattern(regexp = "^[a-zA-Z0-9_-]+$"); + + NotBlank notBlank() default @NotBlank; +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefixValidator.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefixValidator.java new file mode 100644 index 0000000..07ec92e --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/FileNamePrefixValidator.java @@ -0,0 +1,35 @@ +package com.github.nagyesta.filebarj.core.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.hibernate.validator.internal.constraintvalidators.bv.NotBlankValidator; +import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator; + +public class FileNamePrefixValidator implements ConstraintValidator { + + private final PatternValidator patternValidator = new PatternValidator(); + private final NotBlankValidator notBlankValidator = new NotBlankValidator(); + + @Override + public void initialize(final FileNamePrefix constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + patternValidator.initialize(constraintAnnotation.pattern()); + notBlankValidator.initialize(constraintAnnotation.notBlank()); + } + + @Override + public boolean isValid(final String value, final ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + if (!notBlankValidator.isValid(value, context)) { + context.buildConstraintViolationWithTemplate("{jakarta.validation.constraints.NotBlank.message}") + .addConstraintViolation(); + return false; + } + if (!patternValidator.isValid(value, context)) { + context.buildConstraintViolationWithTemplate("{jakarta.validation.constraints.Pattern.message}") + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSeconds.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSeconds.java new file mode 100644 index 0000000..906ea9b --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSeconds.java @@ -0,0 +1,29 @@ +package com.github.nagyesta.filebarj.core.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PastOrPresentEpochSecondsValidator.class) +public @interface PastOrPresentEpochSeconds { + + /** + * @return the error message template + */ + String message() default "{jakarta.validation.constraints.PastOrPresent.message}"; + + /** + * @return the groups the constraint belongs to + */ + Class[] groups() default {}; + + /** + * @return the payload associated to the constraint + */ + Class[] payload() default {}; + +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSecondsValidator.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSecondsValidator.java new file mode 100644 index 0000000..9e5f07b --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/validation/PastOrPresentEpochSecondsValidator.java @@ -0,0 +1,20 @@ +package com.github.nagyesta.filebarj.core.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.time.Instant; +import java.util.Optional; + +public class PastOrPresentEpochSecondsValidator implements ConstraintValidator { + + @Override + public void initialize(final PastOrPresentEpochSeconds constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(final Long value, final ConstraintValidatorContext context) { + return Instant.now().getEpochSecond() >= Optional.ofNullable(value).orElse(Long.MAX_VALUE); + } +} diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImplTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImplTest.java index eb89a65..06354a2 100644 --- a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImplTest.java +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/common/ManifestManagerImplTest.java @@ -2,6 +2,7 @@ import com.github.nagyesta.filebarj.core.TempFileAwareTest; import com.github.nagyesta.filebarj.core.backup.ArchivalException; +import com.github.nagyesta.filebarj.core.backup.worker.FileMetadataParserFactory; import com.github.nagyesta.filebarj.core.config.BackupJobConfiguration; import com.github.nagyesta.filebarj.core.config.BackupSource; import com.github.nagyesta.filebarj.core.config.enums.CompressionAlgorithm; @@ -12,6 +13,7 @@ import com.github.nagyesta.filebarj.core.model.ValidationRules; import com.github.nagyesta.filebarj.core.model.enums.BackupType; import com.github.nagyesta.filebarj.io.stream.crypto.EncryptionUtil; +import jakarta.validation.ValidationException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -19,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.List; import java.util.Set; import static org.mockito.Mockito.mock; @@ -27,7 +30,7 @@ class ManifestManagerImplTest extends TempFileAwareTest { public static final int A_SECOND = 1000; private final BackupJobConfiguration configuration = BackupJobConfiguration.builder() .fileNamePrefix("prefix") - .sources(Set.of()) + .sources(Set.of(BackupSource.builder().path(BackupPath.ofPathAsIs("/tmp")).build())) .compression(CompressionAlgorithm.NONE) .hashAlgorithm(HashAlgorithm.SHA256) .chunkSizeMebibyte(1) @@ -94,6 +97,7 @@ void testLoadShouldReadPreviouslyPersistedManifestWhenUsingEncryption() throws I .encryptionKey(keyPair.getPublic()) .build(); final var expected = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(expected); //when underTest.persist(expected); @@ -126,6 +130,7 @@ void testLoadShouldReadPreviouslyPersistedManifestWhenNotUsingEncryption() throw .backupType(BackupType.FULL) .build(); final var expected = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(expected); //when underTest.persist(expected); @@ -150,7 +155,7 @@ void testLoadShouldFilterOutManifestsAfterThresholdWhenATimeStampIsProvided() final var destinationDirectory = testDataRoot.resolve("destination"); final var config = BackupJobConfiguration.builder() .fileNamePrefix("prefix") - .sources(Set.of(BackupSource.builder().path(BackupPath.ofPathAsIs("/tmp")).build())) + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot)).build())) .compression(CompressionAlgorithm.GZIP) .hashAlgorithm(HashAlgorithm.SHA256) .chunkSizeMebibyte(1) @@ -159,11 +164,13 @@ void testLoadShouldFilterOutManifestsAfterThresholdWhenATimeStampIsProvided() .backupType(BackupType.FULL) .build(); final var expected = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(expected); underTest.persist(expected); Thread.sleep(A_SECOND); final var limit = Instant.now().getEpochSecond(); Thread.sleep(A_SECOND); final var ignored = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(ignored); underTest.persist(ignored); //when @@ -197,9 +204,11 @@ void testLoadShouldFilterOutManifestsBeforeLatestFullBackupWhenMultipleFullBacku .backupType(BackupType.FULL) .build(); final var ignored = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(ignored); underTest.persist(ignored); Thread.sleep(A_SECOND); final var expected = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(expected); underTest.persist(expected); //when @@ -232,8 +241,10 @@ void testLoadShouldThrowExceptionWhenAPreviousVersionIsMissing() throws Interrup .backupType(BackupType.INCREMENTAL) .build(); final var original = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(original); Thread.sleep(A_SECOND); final var secondIncrement = underTest.generateManifest(config, BackupType.INCREMENTAL, 2); + simulateThatADirectoryWasArchived(secondIncrement); underTest.persist(original); underTest.persist(secondIncrement); @@ -298,6 +309,7 @@ void testLoadShouldThrowExceptionWhenCalledWithNullDirectory() { .backupType(BackupType.FULL) .build(); final var manifest = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(manifest); underTest.persist(manifest); //when @@ -324,6 +336,7 @@ void testLoadShouldThrowExceptionWhenCalledWithNullPrefix() { .backupType(BackupType.FULL) .build(); final var manifest = underTest.generateManifest(config, BackupType.FULL, 0); + simulateThatADirectoryWasArchived(manifest); underTest.persist(manifest); //when @@ -371,6 +384,30 @@ void testValidateShouldThrowExceptionWhenCalledWithNullRules() { //then + exception } + @Test + void testValidateShouldThrowExceptionWhenCalledWithInvalidData() { + //given + final var underTest = new ManifestManagerImpl(); + final var destinationDirectory = testDataRoot.resolve("destination"); + final var config = BackupJobConfiguration.builder() + .fileNamePrefix("prefix") + .sources(Set.of(BackupSource.builder().path(BackupPath.ofPathAsIs("/tmp")).build())) + .compression(CompressionAlgorithm.GZIP) + .hashAlgorithm(HashAlgorithm.SHA256) + .chunkSizeMebibyte(1) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .destinationDirectory(destinationDirectory) + .backupType(BackupType.FULL) + .build(); + final var manifest = underTest.generateManifest(config, BackupType.FULL, 0); + + //when + Assertions.assertThrows(ValidationException.class, + () -> underTest.validate(manifest, ValidationRules.Persisted.class)); + + //then + exception + } + @SuppressWarnings("DataFlowIssue") @Test void testMergeForRestoreShouldThrowExceptionWhenCalledWithNull() { @@ -421,4 +458,11 @@ void testLoadPreviousManifestsForBackupShouldThrowExceptionWhenCalledWithNull() //then + exception } + + private void simulateThatADirectoryWasArchived(final BackupIncrementManifest expected) { + expected.setIndexFileName("index"); + expected.setDataFileNames(List.of("data")); + final var directoryMetadata = FileMetadataParserFactory.newInstance().parse(testDataRoot.toFile(), configuration); + expected.getFiles().put(directoryMetadata.getId(), directoryMetadata); + } } diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java index 3afdedd..f9fd0c3 100644 --- a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java @@ -8,6 +8,8 @@ import com.github.nagyesta.filebarj.core.model.BackupPath; import com.github.nagyesta.filebarj.core.model.enums.BackupType; import com.github.nagyesta.filebarj.io.stream.crypto.EncryptionUtil; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,6 +18,9 @@ class BackupJobConfigurationTest extends TempFileAwareTest { + @SuppressWarnings("resource") + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + @Test @SuppressWarnings("checkstyle:MagicNumber") void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfFullyPopulatedObject() throws JsonProcessingException { @@ -70,4 +75,242 @@ void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfMini Assertions.assertEquals(expected.getChunkSizeMebibyte(), actual.getChunkSizeMebibyte()); Assertions.assertIterableEquals(expected.getSources(), actual.getSources()); } + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + void testValidationShouldPassWhenCalledOnFullyPopulatedObject() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.SHA256) + .compression(CompressionAlgorithm.GZIP) + .encryptionKey(EncryptionUtil.generateRsaKeyPair().getPublic()) + .chunkSizeMebibyte(1024) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot, "visible-file1.txt")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(0, actual.size()); + } + + @Test + void testValidationShouldPassWhenCalledOnMinimalObject() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot, "visible-file1.txt")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(0, actual.size()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithEmptyPrefix() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("") + .chunkSizeMebibyte(1) + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot, "visible-file1.txt")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("fileNamePrefix", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals("", actualEntry.getInvalidValue()); + Assertions.assertEquals("must not be blank", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithInvalidPrefix() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("i n v a l i d") + .chunkSizeMebibyte(1) + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot, "visible-file1.txt")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("fileNamePrefix", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals("i n v a l i d", actualEntry.getInvalidValue()); + Assertions.assertEquals("must match \"^[a-zA-Z0-9_-]+$\"", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithInvalidChunkSize() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .chunkSizeMebibyte(0) + .sources(Set.of(BackupSource.builder().path(BackupPath.of(testDataRoot, "visible-file1.txt")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("chunkSizeMebibyte", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals(0, actualEntry.getInvalidValue()); + Assertions.assertEquals("must be greater than 0", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithNoSources() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .chunkSizeMebibyte(1) + .sources(Set.of()) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("sources", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals(Set.of(), actualEntry.getInvalidValue()); + Assertions.assertEquals("size must be between 1 and 2147483647", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithInvalidSourcePath() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .chunkSizeMebibyte(1) + .sources(Set.of(BackupSource.builder().path(BackupPath.ofPathAsIs("")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("sources[].path.path", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals("", actualEntry.getInvalidValue()); + Assertions.assertEquals("must not be blank", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithInvalidSourceIncludePattern() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .chunkSizeMebibyte(1) + .sources(Set.of(BackupSource.builder() + .path(BackupPath.of(testDataRoot, "visible-file1.txt")) + .includePatterns(Set.of("")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("sources[].includePatterns[].", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals("", actualEntry.getInvalidValue()); + Assertions.assertEquals("must not be blank", actualEntry.getMessage()); + } + + @Test + void testValidationShouldFailWhenCalledOnConfigurationWithInvalidSourceExcludePattern() { + //given + final var testRoot = testDataRoot.toAbsolutePath().toString(); + final var underTest = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .hashAlgorithm(HashAlgorithm.NONE) + .compression(CompressionAlgorithm.NONE) + .destinationDirectory(Path.of(testRoot, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .chunkSizeMebibyte(1) + .sources(Set.of(BackupSource.builder() + .path(BackupPath.of(testDataRoot, "visible-file1.txt")) + .excludePatterns(Set.of("")).build())) + .build(); + + //when + final var actual = validator.validate(underTest); + + //then + Assertions.assertEquals(1, actual.size()); + //noinspection OptionalGetWithoutIsPresent + final var actualEntry = actual.stream().findFirst().get(); + Assertions.assertEquals("sources[].excludePatterns[].", actualEntry.getPropertyPath().toString()); + Assertions.assertEquals("", actualEntry.getInvalidValue()); + Assertions.assertEquals("must not be blank", actualEntry.getMessage()); + } } diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/AppVersionTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/AppVersionTest.java index b277171..54cc7d3 100644 --- a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/AppVersionTest.java +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/AppVersionTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -10,6 +12,8 @@ class AppVersionTest { private final ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("resource") + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); @Test void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedState() throws JsonProcessingException { @@ -60,4 +64,63 @@ void testConstructorShouldThrowExceptionWhenStringHasTooFewTokens(final String v //then + exception } + + @Test + void testValidationShouldPassWhenCalledWithValidAppVersion() throws JsonProcessingException { + //given + + //when + final var actual = validator.validate(new AppVersion(0, 1, 2)); + + //then + Assertions.assertEquals(0, actual.size()); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void testValidationShouldFailWhenCalledWithInvalidMajorVersion() throws JsonProcessingException { + //given + + //when + final var actual = validator.validate(new AppVersion(-1, 1, 2)); + + //then + Assertions.assertEquals(1, actual.size()); + final var violation = actual.stream().findFirst().get(); + Assertions.assertEquals("major", violation.getPropertyPath().toString()); + Assertions.assertEquals(-1, violation.getInvalidValue()); + Assertions.assertEquals("must be greater than or equal to 0", violation.getMessage()); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void testValidationShouldFailWhenCalledWithInvalidMinorVersion() throws JsonProcessingException { + //given + + //when + final var actual = validator.validate(new AppVersion(0, -1, 2)); + + //then + Assertions.assertEquals(1, actual.size()); + final var violation = actual.stream().findFirst().get(); + Assertions.assertEquals("minor", violation.getPropertyPath().toString()); + Assertions.assertEquals(-1, violation.getInvalidValue()); + Assertions.assertEquals("must be greater than or equal to 0", violation.getMessage()); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void testValidationShouldFailWhenCalledWithInvalidPatchVersion() throws JsonProcessingException { + //given + + //when + final var actual = validator.validate(new AppVersion(0, 0, -1)); + + //then + Assertions.assertEquals(1, actual.size()); + final var violation = actual.stream().findFirst().get(); + Assertions.assertEquals("patch", violation.getPropertyPath().toString()); + Assertions.assertEquals(-1, violation.getInvalidValue()); + Assertions.assertEquals("must be greater than or equal to 0", violation.getMessage()); + } } diff --git a/file-barj-job/build.gradle.kts b/file-barj-job/build.gradle.kts index 842c4ab..8d48e7d 100644 --- a/file-barj-job/build.gradle.kts +++ b/file-barj-job/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(project(":file-barj-core")) implementation(libs.bundles.jackson) implementation(libs.bundles.logback) + implementation(libs.bundles.validation) implementation(libs.commons.io) implementation(libs.commons.cli) implementation(libs.bouncycastle.bcpkix) @@ -62,6 +63,7 @@ licensee { allow("Apache-2.0") allow("LGPL-2.1-only") allow("BSD-2-Clause") + allow("GPL-2.0-with-classpath-exception") allowUrl("https://www.bouncycastle.org/licence.html") } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/Controller.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/Controller.java index 3e891ce..cc8dd67 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/Controller.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/Controller.java @@ -13,6 +13,9 @@ import com.github.nagyesta.filebarj.io.stream.crypto.EncryptionUtil; import com.github.nagyesta.filebarj.job.cli.*; import com.github.nagyesta.filebarj.job.util.KeyStoreUtil; +import jakarta.validation.Validation; +import jakarta.validation.ValidationException; +import jakarta.validation.Validator; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; @@ -40,6 +43,7 @@ public class Controller { private static final String WHITE = "\033[0;37"; private final String[] args; private final Console console; + private final Validator validator = createValidator(); /** * Creates a new {@link Controller} instance and sets the input arguments. @@ -60,42 +64,49 @@ public void run() throws Exception { final var backupProperties = new CliBackupParser(Arrays.stream(args) .skip(1) .toArray(String[]::new)).getResult(); + validate(backupProperties); doBackup(backupProperties); break; case RESTORE: final var restoreProperties = new CliRestoreParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(restoreProperties); doRestore(restoreProperties); break; case MERGE: final var mergeProperties = new CliMergeParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(mergeProperties); doMerge(mergeProperties); break; case GEN_KEYS: final var keyStoreProperties = new CliKeyGenParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(keyStoreProperties); doGenerateKey(keyStoreProperties); break; case INSPECT_CONTENT: final var inspectContentProperties = new CliInspectContentParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(inspectContentProperties); doInspectContent(inspectContentProperties); break; case INSPECT_INCREMENTS: final var inspectIncrementsProperties = new CliInspectIncrementsParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(inspectIncrementsProperties); doInspectIncrements(inspectIncrementsProperties); break; case DELETE_INCREMENTS: final var deleteIncrementsProperties = new CliDeleteIncrementsParser(Arrays.stream(args) .skip(1) .toArray(String[]::new), console).getResult(); + validate(deleteIncrementsProperties); doDeleteIncrements(deleteIncrementsProperties); break; default: @@ -214,4 +225,21 @@ private PrivateKey getPrivateKey(final KeyStoreProperties keyProperties) { keyStoreProperties.getPassword(), keyStoreProperties.getPassword())) .orElse(null); } + + private void validate(final Object properties) { + final var violations = validator.validate(properties); + if (!violations.isEmpty()) { + final var violationsMessage = violations.stream() + .map(v -> v.getPropertyPath().toString() + ": " + v.getMessage() + " (found: " + v.getInvalidValue() + ")") + .collect(Collectors.joining("\n\t")); + log.error("Properties validation failed:\n\t{}", violationsMessage); + throw new ValidationException("The properties are invalid!"); + } + } + + private static Validator createValidator() { + try (var validatorFactory = Validation.buildDefaultValidatorFactory()) { + return validatorFactory.getValidator(); + } + } } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupFileProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupFileProperties.java index 54bebba..70c3c6e 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupFileProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupFileProperties.java @@ -1,5 +1,7 @@ package com.github.nagyesta.filebarj.job.cli; +import com.github.nagyesta.filebarj.core.validation.FileNamePrefix; +import jakarta.validation.Valid; import lombok.Data; import lombok.NonNull; import lombok.experimental.SuperBuilder; @@ -14,7 +16,9 @@ public class BackupFileProperties { @NonNull private final Path backupSource; + @Valid private final KeyStoreProperties keyProperties; + @FileNamePrefix @NonNull private final String prefix; } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupProperties.java index b1beadf..5fb63e0 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/BackupProperties.java @@ -1,5 +1,6 @@ package com.github.nagyesta.filebarj.job.cli; +import jakarta.validation.constraints.Positive; import lombok.Builder; import lombok.Data; import lombok.NonNull; @@ -14,6 +15,7 @@ public class BackupProperties { @NonNull private final Path config; + @Positive private final int threads; private final boolean forceFullBackup; } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/DeleteIncrementsProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/DeleteIncrementsProperties.java index 643a841..8bf975b 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/DeleteIncrementsProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/DeleteIncrementsProperties.java @@ -1,5 +1,6 @@ package com.github.nagyesta.filebarj.job.cli; +import jakarta.validation.constraints.Positive; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.SuperBuilder; @@ -11,7 +12,7 @@ @SuperBuilder @EqualsAndHashCode(callSuper = true) public class DeleteIncrementsProperties extends BackupFileProperties { - + @Positive private final long afterEpochSeconds; } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/InspectIncrementContentsProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/InspectIncrementContentsProperties.java index 9b4de00..7b772bb 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/InspectIncrementContentsProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/InspectIncrementContentsProperties.java @@ -1,5 +1,6 @@ package com.github.nagyesta.filebarj.job.cli; +import jakarta.validation.constraints.Positive; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; @@ -14,9 +15,8 @@ @SuperBuilder @EqualsAndHashCode(callSuper = true) public class InspectIncrementContentsProperties extends BackupFileProperties { - + @Positive private final long pointInTimeEpochSeconds; - @NonNull private final Path outputFile; } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/MergeProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/MergeProperties.java index 0ccfb5a..bed0dbd 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/MergeProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/MergeProperties.java @@ -1,5 +1,6 @@ package com.github.nagyesta.filebarj.job.cli; +import com.github.nagyesta.filebarj.core.validation.PastOrPresentEpochSeconds; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.SuperBuilder; @@ -12,6 +13,8 @@ @EqualsAndHashCode(callSuper = true) public class MergeProperties extends BackupFileProperties { private final boolean deleteObsoleteFiles; + @PastOrPresentEpochSeconds private final long fromTimeEpochSeconds; + @PastOrPresentEpochSeconds private final long toTimeEpochSeconds; } diff --git a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/RestoreProperties.java b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/RestoreProperties.java index 95c9402..75ac81c 100644 --- a/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/RestoreProperties.java +++ b/file-barj-job/src/main/java/com/github/nagyesta/filebarj/job/cli/RestoreProperties.java @@ -2,6 +2,9 @@ import com.github.nagyesta.filebarj.core.common.PermissionComparisonStrategy; import com.github.nagyesta.filebarj.core.model.BackupPath; +import com.github.nagyesta.filebarj.core.validation.PastOrPresentEpochSeconds; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; @@ -19,10 +22,13 @@ public class RestoreProperties extends BackupFileProperties { @NonNull private final Map targets; + @Positive private final int threads; private final boolean dryRun; private final boolean deleteFilesNotInBackup; + @PastOrPresentEpochSeconds private final long pointInTimeEpochSeconds; + @Valid private final BackupPath includedPath; private final PermissionComparisonStrategy permissionComparisonStrategy; } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f7bc584..8b2e0d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,10 @@ [versions] slf4j = "2.0.13" logback = "1.5.6" -#hibernateValidator = "8.0.1.Final" +jakartaValidationApi = "3.1.0" +jakartaElApi = "6.0.0" +hibernateValidator = "8.0.1.Final" +expressly = "5.0.0" bouncycastle = "1.78.1" commonsCodec = "1.17.0" commonsCompress = "1.26.2" @@ -34,7 +37,10 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" } -#hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernateValidator" } +jakarta-el-api = { module = "jakarta.el:jakarta.el-api", version.ref = "jakartaElApi" } +jakarta-validation-api = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidationApi" } +hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernateValidator" } +expressly = { module = "org.glassfish.expressly:expressly", version.ref = "expressly" } bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } @@ -59,6 +65,7 @@ jackson-dataformat-xml = { module = "com.fasterxml.jackson.dataformat:jackson-da [bundles] logback = ["logback-classic", "logback-core"] +validation = ["jakarta-el-api", "jakarta-validation-api", "hibernate-validator", "expressly"] jackson = ["jackson-core", "jackson-annotations", "jackson-databind", "jackson-dataformat-xml"] [plugins] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 786a9d7..3ca5a09 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -28,14 +28,6 @@ - - - - - - - - @@ -52,14 +44,6 @@ - - - - - - - - @@ -76,11 +60,6 @@ - - - - - @@ -107,6 +86,19 @@ + + + + + + + + + + + + + @@ -363,22 +355,6 @@ - - - - - - - - - - - - - - - - @@ -387,14 +363,6 @@ - - - - - - - - @@ -403,29 +371,11 @@ - - - - - - - - - - - - - - - - - - @@ -434,14 +384,6 @@ - - - - - - - - @@ -451,9 +393,6 @@ - - - @@ -505,14 +444,6 @@ - - - - - - - - @@ -534,11 +465,6 @@ - - - - - @@ -586,14 +512,6 @@ - - - - - - - - @@ -609,11 +527,6 @@ - - - - - @@ -627,14 +540,6 @@ - - - - - - - - @@ -658,11 +563,6 @@ - - - - - @@ -694,14 +594,6 @@ - - - - - - - - @@ -792,14 +684,6 @@ - - - - - - - - @@ -850,14 +734,6 @@ - - - - - - - - @@ -875,9 +751,6 @@ - - - @@ -922,14 +795,6 @@ - - - - - - - - @@ -939,9 +804,6 @@ - - - @@ -954,14 +816,6 @@ - - - - - - - - @@ -1028,14 +882,6 @@ - - - - - - - - @@ -1089,6 +935,14 @@ + + + + + + + + @@ -1107,6 +961,14 @@ + + + + + + + + @@ -1160,14 +1022,6 @@ - - - - - - - - @@ -1176,14 +1030,6 @@ - - - - - - - - @@ -1192,11 +1038,6 @@ - - - - - @@ -1230,14 +1071,6 @@ - - - - - - - - @@ -1264,11 +1097,6 @@ - - - - - @@ -1285,14 +1113,6 @@ - - - - - - - - @@ -1387,14 +1207,6 @@ - - - - - - - - @@ -1411,14 +1223,6 @@ - - - - - - - - @@ -1499,11 +1303,6 @@ - - - - - @@ -1552,14 +1351,6 @@ - - - - - - - - @@ -1762,14 +1553,6 @@ - - - - - - - - @@ -1778,14 +1561,6 @@ - - - - - - - - @@ -1794,24 +1569,11 @@ - - - - - - - - - - - - - @@ -1820,14 +1582,6 @@ - - - - - - - - @@ -1836,14 +1590,6 @@ - - - - - - - - @@ -1852,16 +1598,16 @@ - - - - - + + + + + @@ -2153,11 +1899,6 @@ - - - - - @@ -2322,6 +2063,11 @@ + + + + + @@ -2410,6 +2156,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2492,6 +2269,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2634,14 +2444,6 @@ - - - - - - - - @@ -2776,14 +2578,6 @@ - - - - - - - - @@ -2792,14 +2586,6 @@ - - - - - - - - @@ -2808,24 +2594,11 @@ - - - - - - - - - - - - - @@ -2834,11 +2607,6 @@ - - - - - @@ -2860,14 +2628,6 @@ - - - - - - - - @@ -2894,16 +2654,6 @@ - - - - - - - - - - @@ -2935,11 +2685,6 @@ - - - - - @@ -2981,11 +2726,6 @@ - - - - - @@ -2996,11 +2736,6 @@ - - - - - @@ -3019,21 +2754,11 @@ - - - - - - - - - - @@ -3059,21 +2784,11 @@ - - - - - - - - - - @@ -3082,21 +2797,11 @@ - - - - - - - - - - @@ -3176,14 +2881,6 @@ - - - - - - - -