From 25f32ffada2cdcedc7e1659a0b39ad96f52a92b9 Mon Sep 17 00:00:00 2001 From: Esta Nagy Date: Mon, 25 Sep 2023 23:33:19 +0200 Subject: [PATCH] Configure Gradle - Defines initial versions of JSON model/configuration entities - Creates encryption utility for key generation and key encryption tasks - Implements local file metadat parser - Adds Lombok configuration - Adds tests - Adds basic Abort-Mission configuration - Fixes some Gradle plugin configuration issues {patch} Signed-off-by: Esta Nagy --- build.gradle.kts | 14 +- file-barj-core/build.gradle.kts | 19 +- file-barj-core/lombok.config | 4 + .../main/java/com/github/nagyesta/Main.java | 8 - .../core/backup/FileMetadataParser.java | 20 ++ .../core/backup/FileMetadataParserLocal.java | 85 ++++++ .../core/config/BackupJobConfiguration.java | 107 +++++++ .../filebarj/core/config/BackupSource.java | 146 ++++++++++ .../enums/DuplicateHandlingStrategy.java | 29 ++ .../core/config/enums/HashAlgorithm.java | 43 +++ .../filebarj/core/crypto/CryptoException.java | 18 ++ .../core/crypto/EncryptionKeyUtil.java | 128 ++++++++ .../core/json/PublicKeyDeserializer.java | 22 ++ .../core/json/PublicKeySerializer.java | 19 ++ .../core/model/ArchiveEntryLocator.java | 37 +++ .../core/model/ArchivedFileMetadata.java | 51 ++++ .../core/model/BackupIncrementManifest.java | 109 +++++++ .../filebarj/core/model/FileMetadata.java | 96 ++++++ .../filebarj/core/model/enums/BackupType.java | 22 ++ .../filebarj/core/model/enums/Change.java | 23 ++ .../filebarj/core/model/enums/FileType.java | 53 ++++ file-barj-core/src/test/java/.gitkeep | 0 .../core/MissionOutlineDefinition.java | 19 ++ .../config/BackupJobConfigurationTest.java | 68 +++++ .../core/config/BackupSourceTest.java | 273 ++++++++++++++++++ .../core/crypto/EncryptionKeyUtilTest.java | 136 +++++++++ .../core/model/ArchivedFileMetadataTest.java | 59 ++++ .../filebarj/core/model/FileMetadataTest.java | 63 ++++ file-barj-core/src/test/resources/.gitkeep | 0 .../org.junit.jupiter.api.extension.Extension | 1 + .../test/resources/junit-platform.properties | 4 + file-barj-job/build.gradle.kts | 15 +- file-barj-job/lombok.config | 4 + gradle/libs.versions.toml | 32 +- 34 files changed, 1685 insertions(+), 42 deletions(-) create mode 100644 file-barj-core/lombok.config delete mode 100644 file-barj-core/src/main/java/com/github/nagyesta/Main.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParser.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParserLocal.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/DuplicateHandlingStrategy.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/HashAlgorithm.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/CryptoException.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtil.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeyDeserializer.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeySerializer.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/BackupType.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/Change.java create mode 100644 file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/FileType.java delete mode 100644 file-barj-core/src/test/java/.gitkeep create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/MissionOutlineDefinition.java create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupSourceTest.java create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtilTest.java create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadataTest.java create mode 100644 file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/FileMetadataTest.java delete mode 100644 file-barj-core/src/test/resources/.gitkeep create mode 100644 file-barj-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 file-barj-core/src/test/resources/junit-platform.properties create mode 100644 file-barj-job/lombok.config diff --git a/build.gradle.kts b/build.gradle.kts index 09b4b76..aea4762 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { checkstyle alias(libs.plugins.versioner) alias(libs.plugins.lombok) apply false + alias(libs.plugins.abort.mission) apply false alias(libs.plugins.index.scan) alias(libs.plugins.owasp.dependencycheck) } @@ -79,6 +80,7 @@ subprojects { apply(plugin = "io.freefair.lombok") apply(plugin = "org.sonatype.gradle.plugins.scan") apply(plugin = "org.owasp.dependencycheck") + apply(plugin = "com.github.nagyesta.abort-mission-gradle-plugin") group = rootProject.group version = rootProject.version @@ -98,18 +100,18 @@ subprojects { tasks.jacocoTestReport { reports { xml.required.set(true) - xml.outputLocation.set(layout.buildDirectory.file("/reports/jacoco/report.xml")) + xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/report.xml")) csv.required.set(false) html.required.set(true) - html.outputLocation.set(layout.buildDirectory.dir("/reports/jacoco/html")) + html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/html")) } dependsOn(tasks.test) finalizedBy(tasks.getByName("jacocoTestCoverageVerification")) } tasks.withType().configureEach { - inputs.file(layout.buildDirectory.file("/reports/jacoco/report.xml")) - outputs.file(layout.buildDirectory.file("/reports/jacoco/jacocoTestCoverageVerification")) + inputs.file(layout.buildDirectory.file("reports/jacoco/report.xml")) + outputs.file(layout.buildDirectory.file("reports/jacoco/jacocoTestCoverageVerification")) violationRules { rule { @@ -141,7 +143,7 @@ subprojects { } } doLast { - layout.buildDirectory.file("/reports/jacoco/jacocoTestCoverageVerification").get().asFile.writeText("Passed") + layout.buildDirectory.file("reports/jacoco/jacocoTestCoverageVerification").get().asFile.writeText("Passed") } } @@ -164,7 +166,7 @@ subprojects { tasks.withType().configureEach { configProperties = mutableMapOf( "base_dir" to rootDir.absolutePath.toString(), - "cache_file" to layout.buildDirectory.file("/checkstyle/cacheFile").get().asFile.absolutePath.toString() + "cache_file" to layout.buildDirectory.file("checkstyle/cacheFile").get().asFile.absolutePath.toString() ) checkstyle.toolVersion = rootProject.libs.versions.checkstyle.get() checkstyle.configFile = rootProject.file("config/checkstyle/checkstyle.xml") diff --git a/file-barj-core/build.gradle.kts b/file-barj-core/build.gradle.kts index a004f18..423c057 100644 --- a/file-barj-core/build.gradle.kts +++ b/file-barj-core/build.gradle.kts @@ -2,15 +2,22 @@ plugins { id("java") } -repositories { - mavenCentral() -} - dependencies { - testImplementation(platform("org.junit:junit-bom:5.9.1")) - testImplementation("org.junit.jupiter:junit-jupiter") + implementation(libs.bundles.jackson) + implementation(libs.commons.codec) + implementation(libs.commons.compress) + implementation(libs.commons.crypto) + implementation(libs.commons.io) + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.jupiter) + testImplementation(libs.abort.mission.jupiter) + testImplementation(libs.mockito.core) } tasks.test { useJUnitPlatform() } + +abortMission { + toolVersion = libs.versions.abortMission.get() +} diff --git a/file-barj-core/lombok.config b/file-barj-core/lombok.config new file mode 100644 index 0000000..8a1cf95 --- /dev/null +++ b/file-barj-core/lombok.config @@ -0,0 +1,4 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true +lombok.nonNull.exceptionType = IllegalArgumentException diff --git a/file-barj-core/src/main/java/com/github/nagyesta/Main.java b/file-barj-core/src/main/java/com/github/nagyesta/Main.java deleted file mode 100644 index 25f0203..0000000 --- a/file-barj-core/src/main/java/com/github/nagyesta/Main.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.nagyesta; - -@SuppressWarnings("checkstyle:HideUtilityClassConstructor") -public class Main { - public static void main(final String[] args) { - System.out.println("Hello world!"); - } -} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParser.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParser.java new file mode 100644 index 0000000..8a81bae --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParser.java @@ -0,0 +1,20 @@ +package com.github.nagyesta.filebarj.core.backup; + +import com.github.nagyesta.filebarj.core.config.BackupJobConfiguration; +import com.github.nagyesta.filebarj.core.model.FileMetadata; + +import java.io.File; + +/** + * Parses metadata of Files. + */ +public interface FileMetadataParser { + + /** + * Reads or calculates metadata of a file we need to include in the backup. + * @param file The current {@link File} we need ot evaluate + * @param configuration The backup configuration + * @return the parsed {@link FileMetadata} + */ + FileMetadata parse(File file, BackupJobConfiguration configuration); +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParserLocal.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParserLocal.java new file mode 100644 index 0000000..c76d1f7 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/backup/FileMetadataParserLocal.java @@ -0,0 +1,85 @@ +package com.github.nagyesta.filebarj.core.backup; + +import com.github.nagyesta.filebarj.core.config.BackupJobConfiguration; +import com.github.nagyesta.filebarj.core.model.FileMetadata; +import com.github.nagyesta.filebarj.core.model.enums.Change; +import com.github.nagyesta.filebarj.core.model.enums.FileType; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Optional; + +/** + * Local file specific implementation of the {@link FileMetadataParser}. + */ +public class FileMetadataParserLocal implements FileMetadataParser { + + @Override + public FileMetadata parse(final File file, final BackupJobConfiguration configuration) { + final var posixFileAttributes = posixPermissionsQuietly(file); + final var basicAttributes = basicAttributesQuietly(file); + + return FileMetadata.builder() + .absolutePath(file.toPath().toAbsolutePath()) + .owner(posixFileAttributes.owner().getName()) + .group(posixFileAttributes.group().getName()) + .posixPermissions(PosixFilePermissions.toString(posixFileAttributes.permissions())) + .lastModifiedUtcEpochSeconds(basicAttributes.lastModifiedTime().toInstant().getEpochSecond()) + .originalSizeBytes(basicAttributes.size()) + .fileType(FileType.findForAttributes(basicAttributes)) + .originalChecksum(calculateChecksum(file, configuration)) + .hidden(checkIsHiddenQuietly(file)) + .status(Change.NEW) + .build(); + } + + private PosixFileAttributes posixPermissionsQuietly(final File file) { + try { + return Files.readAttributes(file.toPath(), PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private BasicFileAttributes basicAttributesQuietly(final File file) { + try { + return Files.readAttributes(file.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + private boolean checkIsHiddenQuietly(final File file) { + try { + return Files.isHidden(file.toPath()); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + + private String calculateChecksum(final File file, final BackupJobConfiguration configuration) { + try { + final var messageDigest = Optional.ofNullable(configuration.getChecksumAlgorithm().getAlgorithmName()) + .map(DigestUtils::new); + final var attributes = basicAttributesQuietly(file); + if (messageDigest.isEmpty() || attributes.isOther()) { + return null; + } else { + if (attributes.isSymbolicLink()) { + return messageDigest.get().digestAsHex(Files.readSymbolicLink(file.toPath()).toAbsolutePath()); + } else { + return messageDigest.get().digestAsHex(file); + } + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } +} 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 new file mode 100644 index 0000000..2592659 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupJobConfiguration.java @@ -0,0 +1,107 @@ +package com.github.nagyesta.filebarj.core.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.github.nagyesta.filebarj.core.config.enums.DuplicateHandlingStrategy; +import com.github.nagyesta.filebarj.core.config.enums.HashAlgorithm; +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 lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import java.nio.file.Path; +import java.security.PublicKey; +import java.util.Set; + +/** + * Configuration class defining the parameters of the backup/restore job. + */ +@Data +@EqualsAndHashCode +@Builder +@Jacksonized +public class BackupJobConfiguration { + /** + * The desired backup type which should be used when the job is executed. + *

+ * NOTE: The backup will be automatically a {@link BackupType#FULL} backup + * every time when there is no previous increment or there is a change in + * the backup configuration since the last increment was saved. As a side + * effect, this property is ignored during the first execution after each + * configuration change. + */ + @NonNull + @JsonProperty("backup_type") + private final BackupType backupType; + /** + * The algorithm used for checksum calculations before and after archival. + * Useful for data integrity verifications. + *

+ * NOTE: A change of this value requires a {@link BackupType#FULL} backup + * as the previous increments cannot use a different hash algorithm. + */ + @NonNull + @JsonProperty("checksum_algorithm") + private final HashAlgorithm checksumAlgorithm; + /** + * The public key of an RSA key pair used for encryption. + * The files will be encrypted using automatically generated AES keys (DEK) + * which will be encrypted using the RSA public key (KEK). + *

+ * NOTE: A change of this value requires a {@link BackupType#FULL} backup + * as the previous increments cannot use a different encryption key. + */ + @JsonSerialize(using = PublicKeySerializer.class) + @JsonDeserialize(using = PublicKeyDeserializer.class) + @JsonProperty("encryption_key") + private final PublicKey encryptionKey; + /** + * The strategy used for handling duplicate files. + *

+ * NOTE: A change of this value requires a {@link BackupType#FULL} backup + * as the previous increments cannot use a different duplicate handling + * strategy. + */ + @NonNull + @JsonProperty("duplicate_strategy") + private final DuplicateHandlingStrategy duplicateStrategy; + /** + * The desired maximum chunk size for the backup archive part. + *

+ * NOTE: Using 0 means that the archive won't be chunked. + */ + @EqualsAndHashCode.Exclude + @JsonProperty("chunk_size_mebibyte") + private final int chunkSizeMebibyte; + /** + * The prefix of the backup file names. + *

+ * NOTE: A change of this value requires a {@link BackupType#FULL} backup + * as the previous increments cannot use a different file name prefix. + */ + @NonNull + @JsonProperty("file_name_prefix") + private final String fileNamePrefix; + /** + * The destination where the backup files will be saved. + *

+ * NOTE: A change of this value requires a {@link BackupType#FULL} backup + * as the metadata of the previous increments must be found in the destination + * in order to calculate changes. + */ + @NonNull + @JsonProperty("destination_directory") + private final Path destinationDirectory; + /** + * The source files we want to archive. + */ + @NonNull + @EqualsAndHashCode.Exclude + @JsonProperty("sources") + private final Set 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 new file mode 100644 index 0000000..9a3ae2f --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/BackupSource.java @@ -0,0 +1,146 @@ +package com.github.nagyesta.filebarj.core.config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ext.NioPathDeserializer; +import com.fasterxml.jackson.databind.ext.NioPathSerializer; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import java.io.File; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Represents a backup source root. Can match a file or directory. + */ +@Data +@Builder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BackupSource { + /** + * Universal pattern including all files. + */ + private static final String INCLUDE_ALL_FILES = "**"; + /** + * The path we want to back up. Can be file or directory. + */ + @NonNull + @JsonProperty("path") + @JsonSerialize(using = NioPathSerializer.class) + @JsonDeserialize(using = NioPathDeserializer.class) + private final Path path; + /** + * Optional include patterns for filtering the contents. + * Uses {@link java.nio.file.PathMatcher} with "glob" syntax + * relative to the value of the path field. + */ + @JsonProperty("includePatterns") + private final Set 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("excludePatterns") + private final Set excludePatterns; + + /** + * Lists the matching {@link Path} entries. + * + * @return matching paths + */ + @JsonIgnore + public List listMatchingFilePaths() { + return listFilesRecursive(path.toAbsolutePath()) + .filter(this::includePatternsDoMatch) + .flatMap(this::includeIntermediateDirectories) + .filter(this::excludePatternsDoNotMatch) + .distinct() + .sorted(Comparator.comparing(Path::toAbsolutePath)) + .toList(); + } + + private Stream includeIntermediateDirectories(final Path aPath) { + final Stream pathAsStream = Stream.of(aPath); + if (aPath.toAbsolutePath().equals(path)) { + return pathAsStream; + } else { + return Stream.of(pathAsStream, includeIntermediateDirectories(aPath.getParent())) + .flatMap(Function.identity()); + } + } + + private Stream listFilesRecursive(final Path fromRoot) { + if (!fromRoot.toFile().exists()) { + return Stream.empty(); + } else if (!Files.isDirectory(fromRoot) || hasNoChildren(fromRoot)) { + return Stream.of(fromRoot); + } else { + return Optional.ofNullable(fromRoot.toFile().listFiles()) + .stream() + .flatMap(Arrays::stream) + .map(File::toPath) + .flatMap(this::listFilesRecursive); + } + } + + private boolean hasNoChildren(final Path dirPath) { + return Optional.ofNullable(dirPath.toFile().list()) + .map(List::of) + .orElse(Collections.emptyList()) + .isEmpty(); + } + + private boolean includePatternsDoMatch(final Path toFilter) { + if (!path.toFile().isDirectory()) { + assertHasNoPatterns(includePatterns, "Include"); + return true; + } + final FileSystem fileSystem = FileSystems.getDefault(); + return Optional.ofNullable(includePatterns) + .filter(v -> !v.isEmpty()) + .orElse(Set.of(INCLUDE_ALL_FILES)) + .stream() + .map(this::translatePattern) + .map(fileSystem::getPathMatcher) + .anyMatch(matcher -> matcher.matches(toFilter.toAbsolutePath())); + } + + private boolean excludePatternsDoNotMatch(final Path toFilter) { + if (!path.toFile().isDirectory()) { + assertHasNoPatterns(excludePatterns, "Exclude"); + return true; + } + final FileSystem fileSystem = FileSystems.getDefault(); + return Optional.ofNullable(excludePatterns) + .orElse(Set.of()) + .stream() + .map(this::translatePattern) + .map(fileSystem::getPathMatcher) + .noneMatch(matcher -> matcher.matches(toFilter.toAbsolutePath())); + } + + private void assertHasNoPatterns(final Set patterns, final String prefix) { + if (!Optional.ofNullable(patterns).orElse(Collections.emptySet()).isEmpty()) { + throw new IllegalArgumentException( + prefix + " patterns cannot be defined when the backup source is not a directory: " + path); + } + + } + + private String translatePattern(final String pattern) { + return "glob:" + path.toAbsolutePath() + File.separator + pattern; + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/DuplicateHandlingStrategy.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/DuplicateHandlingStrategy.java new file mode 100644 index 0000000..31dcda1 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/DuplicateHandlingStrategy.java @@ -0,0 +1,29 @@ +package com.github.nagyesta.filebarj.core.config.enums; + +/** + * Defines the strategy used in case a file is found in more than one place. + */ +public enum DuplicateHandlingStrategy { + /** + * Archives each copies as separate entry in the archive. + *
e.g.,
+ * Each duplicate is added as many times as it is found in the source. + */ + KEEP_EACH, + /** + * Archives one copy for each backup increment. + *
e.g.,
+ * The second instance of the same file is not added to the current + * backup increment if it was already saved once. Each duplicate can + * point to the same archive file. + */ + KEEP_ONE_PER_INCREMENT, + /** + * Archives one copy per any increment of the backup since the last full backup. + *
e.g.,
+ * The file is not added to the current archive even if the duplicate + * is found archived in a previous backup version, such as a file was + * overwritten with a previously archived version of the same file, + */ + KEEP_ONE_PER_BACKUP +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/HashAlgorithm.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/HashAlgorithm.java new file mode 100644 index 0000000..8f54f60 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/config/enums/HashAlgorithm.java @@ -0,0 +1,43 @@ +package com.github.nagyesta.filebarj.core.config.enums; + +import lombok.Getter; +import lombok.ToString; + +/** + * Defines the supported hash algorithms used for checksum calculations. + */ +@Getter +@ToString +public enum HashAlgorithm { + /** + * No checksum calculation needed. + */ + NONE(null), + /** + * MD5. + */ + MD5("MD5"), + /** + * SHA-1. + */ + SHA1("SHA-1"), + /** + * SHA-256. + */ + SHA256("SHA-256"), + /** + * SHA-512. + */ + SHA512("SHA-512"); + + private final String algorithmName; + + /** + * Constructs an enum for the provided algorithm. + * + * @param algorithmName The algorithm. + */ + HashAlgorithm(final String algorithmName) { + this.algorithmName = algorithmName; + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/CryptoException.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/CryptoException.java new file mode 100644 index 0000000..5f122bc --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/CryptoException.java @@ -0,0 +1,18 @@ +package com.github.nagyesta.filebarj.core.crypto; + +/** + * Exception thrown when a crypto operation fails. + */ +public class CryptoException extends RuntimeException { + + /** + * Creates a new instance and initializes it with the specified message + * and cause. + * + * @param message the message + * @param cause the cause + */ + public CryptoException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtil.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtil.java new file mode 100644 index 0000000..4d2b875 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtil.java @@ -0,0 +1,128 @@ +package com.github.nagyesta.filebarj.core.crypto; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.*; +import java.security.spec.EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import static java.security.spec.MGF1ParameterSpec.SHA256; +import static javax.crypto.spec.PSource.PSpecified.DEFAULT; + +/** + * Utility for basic Key generation and encryption steps. + */ +@UtilityClass +public class EncryptionKeyUtil { + private static final String RSA_ALG = "RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"; + private static final int RSA_KEY_SIZE = 4096; + private static final String AES = "AES"; + private static final int KEY_SIZE_BYTES = 256 / 8; + private static final String RSA = "RSA"; + private static final String SHA_256 = "SHA-256"; + private static final String MGF_1 = "MGF1"; + + /** + * Decrypts the given encrypted byte array using the provided private key. + * + * @param privateKey the private key used for decryption + * @param encrypted the byte array to be decrypted + * @return the decrypted byte array + */ + public byte[] decryptBytes(@NonNull final PrivateKey privateKey, final byte[] encrypted) { + try { + final Cipher cipher = Cipher.getInstance(RSA_ALG); + final OAEPParameterSpec oaepParam = new OAEPParameterSpec(SHA_256, MGF_1, SHA256, DEFAULT); + cipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParam); + return cipher.doFinal(encrypted); + } catch (final Exception e) { + throw new CryptoException("Failed to decrypt encrypted bytes.", e); + } + } + + /** + * Encrypts the given byte array using the provided public key. + * + * @param publicKey the public key used for encryption + * @param bytes the byte array to be encrypted + * @return the encrypted byte array + */ + public byte[] encryptBytes(@NonNull final PublicKey publicKey, final byte[] bytes) { + try { + final Cipher cipher = Cipher.getInstance(RSA_ALG); + final OAEPParameterSpec oaepParam = new OAEPParameterSpec(SHA_256, MGF_1, SHA256, DEFAULT); + cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParam); + return cipher.doFinal(bytes); + } catch (final Exception e) { + throw new CryptoException("Failed to encrypt bytes.", e); + } + } + + /** + * Generates a random key using the AES algorithm. + * + * @return the generated key + */ + public static SecretKey generateAesKey() { + final byte[] secureRandomKeyBytes = generateSecureRandomBytes(); + return byteArrayToAesKey(secureRandomKeyBytes); + } + + /** + * Generates random bytes with a secure random generator.. + * + * @return the random bytes + */ + public static byte[] generateSecureRandomBytes() { + final byte[] secureRandomKeyBytes = new byte[KEY_SIZE_BYTES]; + final SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(secureRandomKeyBytes); + return secureRandomKeyBytes; + } + + /** + * Generates a random key pair using the RSA algorithm. + * + * @return the generated key pair + */ + public static KeyPair generateRsaKeyPair() { + try { + final KeyPairGenerator generator = KeyPairGenerator.getInstance(RSA); + generator.initialize(RSA_KEY_SIZE); + return generator.generateKeyPair(); + } catch (final NoSuchAlgorithmException e) { + throw new CryptoException("Unable to generate RSA key pair.", e); + } + } + + /** + * Converts a byte array to an AES key. + * + * @param bytes the byte array to convert + * @return the AES key + */ + public static SecretKey byteArrayToAesKey(final byte[] bytes) { + return new SecretKeySpec(bytes, AES); + } + + /** + * Converts a byte array to an RSA public key. + * + * @param bytes the byte array to convert + * @return the RSA public key + */ + public static PublicKey byteArrayToRsaPublicKey(final byte[] bytes) { + try { + final KeyFactory keyFactory = KeyFactory.getInstance(RSA); + final EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(bytes); + return keyFactory.generatePublic(publicKeySpec); + } catch (final Exception e) { + throw new CryptoException("Unable to deserialize RSA key", e); + } + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeyDeserializer.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeyDeserializer.java new file mode 100644 index 0000000..89724c8 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeyDeserializer.java @@ -0,0 +1,22 @@ +package com.github.nagyesta.filebarj.core.json; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.github.nagyesta.filebarj.core.crypto.EncryptionKeyUtil; + +import java.io.IOException; +import java.security.PublicKey; +import java.util.Base64; + +/** + * Deserializer for RSA {@link PublicKey} objects. + */ +public class PublicKeyDeserializer extends JsonDeserializer { + @Override + public PublicKey deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException { + final String base64 = p.getValueAsString(); + final byte[] encodedKey = Base64.getDecoder().decode(base64); + return EncryptionKeyUtil.byteArrayToRsaPublicKey(encodedKey); + } +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeySerializer.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeySerializer.java new file mode 100644 index 0000000..3a5db10 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/json/PublicKeySerializer.java @@ -0,0 +1,19 @@ +package com.github.nagyesta.filebarj.core.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.security.PublicKey; +import java.util.Base64; + +/** + * Serializer for {@link PublicKey} objects. + */ +public class PublicKeySerializer extends JsonSerializer { + @Override + public void serialize(final PublicKey value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { + gen.writeString(Base64.getEncoder().encodeToString(value.getEncoded())); + } +} 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 new file mode 100644 index 0000000..c7d801d --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchiveEntryLocator.java @@ -0,0 +1,37 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +/** + * Provides a pointer identifying the location where the archived entry is stored. + */ +@Data +@Builder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ArchiveEntryLocator { + /** + * The backup increment containing the entry. + */ + @JsonProperty("backup_increment") + private final int backupIncrement; + /** + * The name of the entry (file) stored within the archive. + */ + @NonNull + @JsonProperty("entry_name") + private final UUID entryName; + /** + * The random bytes used during encryption. + */ + @JsonProperty("random_bytes") + private final byte[] randomBytes; + +} 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 new file mode 100644 index 0000000..c02b8cd --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadata.java @@ -0,0 +1,51 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import java.util.Set; +import java.util.UUID; + +/** + * Contains information about an archived entry. + */ +@Data +@Builder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ArchivedFileMetadata { + /** + * The unique Id of the metadata record. + */ + @NonNull + @JsonProperty("id") + private final UUID id; + /** + * The location where the archived file contents are stored. + */ + @NonNull + @JsonProperty("archive_location") + private final ArchiveEntryLocator archiveLocation; + /** + * The checksum of the archived content. + */ + @JsonProperty("archived_checksum") + private String archivedChecksum; + /** + * The checksum of the original content. + */ + @JsonProperty("original_checksum") + private String originalChecksum; + /** + * The Ids of the original files which are archived by the + * current entry. If multiple Ids are listed, then duplicates + * where eliminated. + */ + @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 new file mode 100644 index 0000000..8a04dd9 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/BackupIncrementManifest.java @@ -0,0 +1,109 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.filebarj.core.config.BackupJobConfiguration; +import com.github.nagyesta.filebarj.core.crypto.EncryptionKeyUtil; +import com.github.nagyesta.filebarj.core.model.enums.BackupType; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Models the root of the backup increment metadata. + *

+ * This manifest contains every piece of metadata known about the + * original files and their archived variants. + */ +@Data +@Builder +@Jacksonized +public class BackupIncrementManifest { + /** + * The version numbers of the backup increments. + *

+ * THe full backups use the index 0, every subsequent incremental + * backup increments the version by 1. A manifest can contain more + * numbers if the backup increments were merged (consolidated) + * into a single archive. + */ + @NonNull + @JsonProperty("versions") + private Set versions; + /** + * The time when the backup process was started in UTC epoch + * seconds. + */ + @JsonProperty("start_time_utc_epoch_seconds") + private long startTimeUtcEpochSeconds; + /** + * The file name prefix used by the backup archives. + */ + @NonNull + @JsonProperty("file_name_prefix") + private String fileNamePrefix; + /** + * The type of the backup. + */ + @NonNull + @JsonProperty("backup_type") + private BackupType backupType; + /** + * The snapshot of the backup configuration at the time of backup. + */ + @NonNull + @JsonProperty("job_configuration") + private BackupJobConfiguration configuration; + /** + * The map of matching files identified during backup keyed by Id. + */ + @JsonProperty("files") + private Map files; + /** + * The map of archive entries saved during backup keyed by Id.. + */ + @JsonProperty("archive_entries") + private Map archivedEntries; + /** + * The byte array containing the data encryption key (DEK) + * encrypted with the key encryption key (KEK). + */ + @JsonProperty("encryption_key") + private byte[] encryptionKey; + + /** + * Decrypts the byte array stored in {@link #encryptionKey} using the + * provided kekPrivateKey. + * + * @param kekPrivateKey The private key we need to use for decryption. + * @return The decrypted DEK + */ + @JsonIgnore + public SecretKey dataEncryptionKey(final PrivateKey kekPrivateKey) { + final byte[] decryptedBytes = EncryptionKeyUtil.decryptBytes(kekPrivateKey, encryptionKey); + return EncryptionKeyUtil.byteArrayToAesKey(decryptedBytes); + } + + /** + * Generates a new DEK and overwrites the value stored in the + * {@link #encryptionKey} field after encrypting the DEK with the + * provided KEK. + * + * @param kekPublicKey The KEK we will use for encrypting the DEK. + * @return The generated DEK. + */ + @JsonIgnore + public SecretKey generateDataEncryptionKey(final PublicKey kekPublicKey) { + final SecretKey secureRandomKey = EncryptionKeyUtil.generateAesKey(); + encryptionKey = EncryptionKeyUtil.encryptBytes(kekPublicKey, secureRandomKey.getEncoded()); + return secureRandomKey; + } +} 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 new file mode 100644 index 0000000..91f69e1 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/FileMetadata.java @@ -0,0 +1,96 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +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 lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.jackson.Jacksonized; + +import java.nio.file.Path; +import java.util.UUID; + +/** + * Contains information about a file from the scope of the backup + * increment. + */ +@Data +@Builder +@Jacksonized +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileMetadata { + /** + * The unique Id of the file. + */ + @NonNull + @JsonProperty("id") + private final UUID id; + /** + * The absolute path where the file is located. + */ + @NonNull + @JsonProperty("path") + private final Path absolutePath; + /** + * The checksum of the file content using the configured checksum + * algorithm. + *
+ * {@link com.github.nagyesta.filebarj.core.config.BackupJobConfiguration#getChecksumAlgorithm()} + */ + @JsonProperty("original_checksum") + private final String originalChecksum; + /** + * The original file size. + */ + @JsonProperty("original_size") + private Long originalSizeBytes; + /** + * The last modified time of the file using UTC epoch seconds. + */ + @JsonProperty("last_modified_utc_epoch_seconds") + private Long lastModifiedUtcEpochSeconds; + /** + * The POSIX permissions of the file. + */ + @JsonProperty("permissions") + private final String posixPermissions; + /** + * The owner of the file. + */ + @JsonProperty("owner") + private final String owner; + /** + * The owner group of the file. + */ + @JsonProperty("group") + private final String group; + /** + * The file type (file/directory/symbolic link/other). + */ + @NonNull + @JsonProperty("file_type") + private final FileType fileType; + /** + * The hidden status of the file. + */ + @JsonProperty("hidden") + private Boolean hidden; + /** + * The detected change status of the file. + */ + @NonNull + @JsonProperty("status") + private Change status; + /** + * The Id of the archive metadata for the entity storing this file. + */ + @JsonProperty("archive_metadata_id") + private UUID archiveMetadataId; + /** + * An optional error message in case of blocker issues during backup. + */ + @JsonProperty("error") + private String error; +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/BackupType.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/BackupType.java new file mode 100644 index 0000000..b87bfe6 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/BackupType.java @@ -0,0 +1,22 @@ +package com.github.nagyesta.filebarj.core.model.enums; + +/** + * The type of the backup task. + */ +public enum BackupType { + /** + * Saves every file without considering any of the previous state. + *
+ * Ignores previous backups of the same state. Mandatory after configuration + * changes. + */ + FULL, + /** + * Saves only the delta identified since the last backup increment. + *
+ * The previous increment may be either a {@link #FULL} or {@code INCREMENTAL} + * backup. The current increment will consider only the changes since the last + * increment in either case. + */ + INCREMENTAL +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/Change.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/Change.java new file mode 100644 index 0000000..c8bd88e --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/Change.java @@ -0,0 +1,23 @@ +package com.github.nagyesta.filebarj.core.model.enums; + +/** + * Indicates the change status of a file. + */ +public enum Change { + /** + * The file was missing from the previous backup, but it exists now. + */ + NEW, + /** + * The file was present in the previous backup and did not change since. + */ + NO_CHANGE, + /** + * The file was present in the previous backup, but it changed since. + */ + MODIFIED, + /** + * The file was present in the previous backup, but it is missing now (probably because it got deleted). + */ + DELETED +} diff --git a/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/FileType.java b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/FileType.java new file mode 100644 index 0000000..b620e80 --- /dev/null +++ b/file-barj-core/src/main/java/com/github/nagyesta/filebarj/core/model/enums/FileType.java @@ -0,0 +1,53 @@ +package com.github.nagyesta.filebarj.core.model.enums; + +import lombok.NonNull; + +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.function.Predicate; + +/** + * Represents the type of the file. + */ +public enum FileType { + /** + * Regular file. + */ + REGULAR_FILE(BasicFileAttributes::isRegularFile), + /** + * Directory. + */ + DIRECTORY(BasicFileAttributes::isDirectory), + /** + * Symbolic link. + */ + SYMBOLIC_LINK(BasicFileAttributes::isSymbolicLink), + /** + * Other (for example a device). + */ + OTHER(BasicFileAttributes::isOther); + + private final Predicate test; + + /** + * Constructs an enum and sets the matching predicate. + * + * @param test The matching predicate. + */ + FileType(final Predicate test) { + this.test = test; + } + + /** + * Finds a suitable {@link FileType} based on the provided attributes. + * + * @param attributes The attributes. + * @return The file type. + */ + public static FileType findForAttributes(@NonNull final BasicFileAttributes attributes) { + return Arrays.stream(values()) + .filter(f -> f.test.test(attributes)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unable to find matching file type.")); + } +} diff --git a/file-barj-core/src/test/java/.gitkeep b/file-barj-core/src/test/java/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/MissionOutlineDefinition.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/MissionOutlineDefinition.java new file mode 100644 index 0000000..c80f6f5 --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/MissionOutlineDefinition.java @@ -0,0 +1,19 @@ +package com.github.nagyesta.filebarj.core; + +import com.github.nagyesta.abortmission.core.AbortMissionCommandOps; +import com.github.nagyesta.abortmission.core.MissionControl; +import com.github.nagyesta.abortmission.core.outline.MissionOutline; + +import java.util.Map; +import java.util.function.Consumer; + +import static com.github.nagyesta.abortmission.core.MissionControl.reportOnlyEvaluator; + +public class MissionOutlineDefinition extends MissionOutline { + @Override + protected Map> defineOutline() { + return Map.of(SHARED_CONTEXT, ops -> { + ops.registerHealthCheck(reportOnlyEvaluator(MissionControl.matcher().anyClass().build()).build()); + }); + } +} 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 new file mode 100644 index 0000000..8566ac1 --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupJobConfigurationTest.java @@ -0,0 +1,68 @@ +package com.github.nagyesta.filebarj.core.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.filebarj.core.config.enums.DuplicateHandlingStrategy; +import com.github.nagyesta.filebarj.core.config.enums.HashAlgorithm; +import com.github.nagyesta.filebarj.core.crypto.EncryptionKeyUtil; +import com.github.nagyesta.filebarj.core.model.enums.BackupType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.Set; + +class BackupJobConfigurationTest { + + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfFullyPopulatedObject() throws JsonProcessingException { + //given + final BackupJobConfiguration expected = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .checksumAlgorithm(HashAlgorithm.SHA256) + .encryptionKey(EncryptionKeyUtil.generateRsaKeyPair().getPublic()) + .chunkSizeMebibyte(1024) + .destinationDirectory(Path.of(TEMP_DIR, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .sources(Set.of(BackupSource.builder().path(Path.of(TEMP_DIR, "visible-file1.txt")).build())) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final BackupJobConfiguration actual = objectMapper.readerFor(BackupJobConfiguration.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + Assertions.assertEquals(expected.getChunkSizeMebibyte(), actual.getChunkSizeMebibyte()); + Assertions.assertIterableEquals(expected.getSources(), actual.getSources()); + } + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfMinimalObject() throws JsonProcessingException { + //given + final BackupJobConfiguration expected = BackupJobConfiguration.builder() + .backupType(BackupType.FULL) + .checksumAlgorithm(HashAlgorithm.NONE) + .destinationDirectory(Path.of(TEMP_DIR, "file-barj")) + .duplicateStrategy(DuplicateHandlingStrategy.KEEP_EACH) + .fileNamePrefix("backup-") + .sources(Set.of(BackupSource.builder().path(Path.of(TEMP_DIR, "visible-file1.txt")).build())) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final BackupJobConfiguration actual = objectMapper.readerFor(BackupJobConfiguration.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + Assertions.assertEquals(expected.getChunkSizeMebibyte(), actual.getChunkSizeMebibyte()); + Assertions.assertIterableEquals(expected.getSources(), actual.getSources()); + } +} diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupSourceTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupSourceTest.java new file mode 100644 index 0000000..1a18fe4 --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/config/BackupSourceTest.java @@ -0,0 +1,273 @@ +package com.github.nagyesta.filebarj.core.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class BackupSourceTest { + + private static Path testDataRoot; + private static final List DIRS_RELATIVE = List.of( + "", + ".hidden", ".hidden/dir1", ".hidden/dir2", + "visible", "visible/dir1", "visible/dir2", + "tmp", "tmp/ignored"); + private static final List FILES_RELATIVE = List.of( + ".hidden-file1.txt", + "visible-file1.txt", + ".hidden/file3.txt", ".hidden/dir1/1.txt", ".hidden/dir2/1.md", + "visible/1.txt", "visible/dir1/1.txt", + "tmp/1.txt"); + private static List dirsCreated; + private static List filesCreated; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeAll + static void beforeAll() { + final String tempDir = System.getProperty("java.io.tmpdir"); + testDataRoot = Path.of(tempDir, "backup-source-" + UUID.randomUUID()); + dirsCreated = DIRS_RELATIVE.stream() + .map(p -> Path.of(testDataRoot.toString() + File.separator + p).toFile()) + .map(f -> { + Assertions.assertTrue(f.mkdir(), "Directory was already found: " + f.getAbsolutePath()); + f.deleteOnExit(); + return f.toPath(); + }) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + filesCreated = FILES_RELATIVE.stream() + .map(p -> Path.of(testDataRoot.toString() + File.separator + p).toFile()) + .map(f -> { + Assertions.assertDoesNotThrow(() -> Assertions + .assertTrue(f.createNewFile(), "File was already found: " + f.getAbsolutePath())); + f.deleteOnExit(); + return f.toPath(); + }) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + + @AfterAll + static void afterAll() { + Stream.of(filesCreated, dirsCreated) + .flatMap(List::stream) + .map(Path::toFile) + .forEach(file -> Assertions.assertTrue(file.delete(), "Could not delete: " + file.getAbsolutePath())); + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream filterExpressionProvider() { + return Stream.builder() + .add(Arguments.of(Set.of("**/*.txt"), Set.of("**/dir2/**", "tmp", "tmp/**"), 9, ".txt")) + .add(Arguments.of(Set.of("**/*.md"), Set.of(), 4, ".md")) + .add(Arguments.of(Set.of(".hidden/**"), Set.of("**/*.md", "**/*.txt"), 4, "!!!NONE-MATCH!!!")) + .add(Arguments.of(Set.of(".hidden*.txt"), Set.of(), 2, ".hidden-file1.txt")) + .build(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream emptyDirectoryFilterExpressionProvider() { + return Stream.builder() + .add(Arguments.of(Set.of("**/*.jpg"), Set.of(), 0)) + .add(Arguments.of(Set.of(), Set.of("**.txt", "**.md"), 9)) + .add(Arguments.of(Set.of("visible/**"), Set.of("**/*.txt", "**/*.md"), 4)) + .build(); + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream nullFilterExpressionProvider() { + return Stream.builder() + .add(Arguments.of(null, null, 17)) + .add(Arguments.of(null, Set.of("**.txt"), 10)) + .add(Arguments.of(Set.of("visible/**"), null, 6)) + .build(); + } + + @ParameterizedTest + @MethodSource("filterExpressionProvider") + void testListMatchingFilePathsShouldOnlyReturnMatchingFilesAndTheirParentsWhenFilteringIsUsed( + final Set includePatterns, + final Set excludePatterns, + final int expectedResults, + final String expectedExtension + ) { + //given + final BackupSource underTest = BackupSource.builder() + .path(testDataRoot) + .excludePatterns(excludePatterns) + .includePatterns(includePatterns) + .build(); + + //when + final List actual = underTest.listMatchingFilePaths(); + + //then + Assertions.assertEquals(expectedResults, actual.size()); + actual.forEach(path -> { + if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { + Assertions.assertTrue(path.toString().endsWith(expectedExtension), + "File should be " + expectedExtension + " but found: " + path); + } else { + Assertions.assertTrue(Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS), + "File should be a directory but wasn't: " + path); + } + }); + } + + @ParameterizedTest + @MethodSource("emptyDirectoryFilterExpressionProvider") + void testListMatchingFilePathsShouldReturnEmptyDirectoriesWhenTheirChildrenAreFilteredOut( + final Set includePatterns, + final Set excludePatterns, + final int expectedResults + ) { + //given + final BackupSource underTest = BackupSource.builder() + .path(testDataRoot) + .excludePatterns(excludePatterns) + .includePatterns(includePatterns) + .build(); + + //when + final List actual = underTest.listMatchingFilePaths(); + + //then + Assertions.assertEquals(expectedResults, actual.size()); + actual.forEach(path -> { + Assertions.assertTrue(Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS), + "File should be a directory but wasn't: " + path); + }); + } + + @ParameterizedTest + @MethodSource("nullFilterExpressionProvider") + void testListMatchingFilePathsShouldUseDefaultFiltersWhenNullPatternSetIsSupplied( + final Set includePatterns, + final Set excludePatterns, + final int expectedResults + ) { + //given + final BackupSource underTest = BackupSource.builder() + .path(testDataRoot) + .excludePatterns(excludePatterns) + .includePatterns(includePatterns) + .build(); + + //when + final List actual = underTest.listMatchingFilePaths(); + + //then + Assertions.assertEquals(expectedResults, actual.size()); + } + + @Test + void testListMatchingFilePathsShouldReturnSingleFileWhenRootIsRegularFile() { + //given + final Path expectedFile = Path.of(testDataRoot.toString(), ".hidden-file1.txt"); + final BackupSource underTest = BackupSource.builder() + .path(expectedFile) + .build(); + + //when + final List actual = underTest.listMatchingFilePaths(); + + //then + Assertions.assertIterableEquals(List.of(expectedFile), actual); + } + + @Test + void testListMatchingFilePathsShouldReturnNothingWhenRootDoesNotExist() { + //given + final Path expectedFile = Path.of(testDataRoot.toString(), "unknown-file.txt"); + final BackupSource underTest = BackupSource.builder() + .path(expectedFile) + .build(); + + //when + final List actual = underTest.listMatchingFilePaths(); + + //then + Assertions.assertIterableEquals(List.of(), actual); + } + + @Test + void testListMatchingFilePathsShouldThrowExceptionWhenIncludePatternsAreSuppliedAndRootIsRegularFile() { + //given + final Path expectedFile = Path.of(testDataRoot.toString(), "visible-file1.txt"); + final BackupSource underTest = BackupSource.builder() + .path(expectedFile) + .includePatterns(Set.of("**.txt")) + .build(); + + //when + Assertions.assertThrows(IllegalArgumentException.class, underTest::listMatchingFilePaths); + + //then + exception + } + + @Test + void testListMatchingFilePathsShouldThrowExceptionWhenExcludePatternsAreSuppliedAndRootIsRegularFile() { + //given + final Path expectedFile = Path.of(testDataRoot.toString(), "visible-file1.txt"); + final BackupSource underTest = BackupSource.builder() + .path(expectedFile) + .excludePatterns(Set.of("**.txt")) + .build(); + + //when + Assertions.assertThrows(IllegalArgumentException.class, underTest::listMatchingFilePaths); + + //then + exception + } + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfFullyPopulatedObject() throws JsonProcessingException { + //given + final BackupSource expected = BackupSource.builder() + .path(testDataRoot) + .includePatterns(Set.of("visible/**")) + .excludePatterns(Set.of("**.txt")) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final BackupSource actual = objectMapper.readerFor(BackupSource.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfMinimalObject() throws JsonProcessingException { + //given + final BackupSource expected = BackupSource.builder() + .path(Path.of(testDataRoot.toString(), "visible-file1.txt")) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final BackupSource actual = objectMapper.readerFor(BackupSource.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } +} diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtilTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtilTest.java new file mode 100644 index 0000000..6a8c411 --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/crypto/EncryptionKeyUtilTest.java @@ -0,0 +1,136 @@ +package com.github.nagyesta.filebarj.core.crypto; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.stream.Stream; + +class EncryptionKeyUtilTest { + + private static final int RSA_KEY_SIZE = 2048; + private static final String RSA = "RSA"; + + public static Stream rsaCryptoProvider() { + final KeyPair keyPair = EncryptionKeyUtil.generateRsaKeyPair(); + return Stream.builder() + .add(Arguments.of(keyPair, "")) + .add(Arguments.of(keyPair, "a")) + .add(Arguments.of(keyPair, "ab")) + .add(Arguments.of(keyPair, "abcd")) + .add(Arguments.of(keyPair, "abcdefgh")) + .add(Arguments.of(keyPair, "lorem ipsum a longer text we will encrypt")) + .build(); + } + + @ParameterizedTest + @MethodSource("rsaCryptoProvider") + void testDecryptBytesShouldReturnOriginalBytesWhenCalledOnOutputOfEncryptBytes( + final KeyPair keyPair, final String expected) { + //given + final byte[] encrypted = EncryptionKeyUtil.encryptBytes(keyPair.getPublic(), expected.getBytes()); + + //when + final byte[] actualBytes = EncryptionKeyUtil.decryptBytes(keyPair.getPrivate(), encrypted); + + //then + final String actual = new String(actualBytes); + Assertions.assertEquals(expected, actual); + } + + @SuppressWarnings("DataFlowIssue") + @Test + void testDecryptBytesShouldThrowExceptionWhenCalledWithNullKey() { + //given + final byte[] bytes = EncryptionKeyUtil.generateSecureRandomBytes(); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> EncryptionKeyUtil.decryptBytes(null, bytes)); + + //then + exception + } + + @Test + void testDecryptBytesShouldThrowExceptionWhenCalledWithNullBytes() { + //given + final KeyPair keyPair = EncryptionKeyUtil.generateRsaKeyPair(); + + //when + Assertions.assertThrows(CryptoException.class, () -> EncryptionKeyUtil.decryptBytes(keyPair.getPrivate(), null)); + + //then + exception + } + + @SuppressWarnings("DataFlowIssue") + @Test + void testEncryptBytesShouldThrowExceptionWhenCalledWithNullKey() { + //given + final byte[] bytes = EncryptionKeyUtil.generateSecureRandomBytes(); + + //when + Assertions.assertThrows(IllegalArgumentException.class, () -> EncryptionKeyUtil.encryptBytes(null, bytes)); + + //then + exception + } + + @Test + void testEncryptBytesShouldThrowExceptionWhenCalledWithNullBytes() { + //given + final KeyPair keyPair = EncryptionKeyUtil.generateRsaKeyPair(); + + //when + Assertions.assertThrows(CryptoException.class, () -> EncryptionKeyUtil.encryptBytes(keyPair.getPublic(), null)); + + //then + exception + } + + @Test + void testGenerateAesKeyShouldReturnAGeneratedSecretKeyWhenCalled() { + //given + + //when + final SecretKey actual = EncryptionKeyUtil.generateAesKey(); + + //then + Assertions.assertEquals("AES", actual.getAlgorithm()); + } + + @Test + void testByteArrayToAesKeyShouldReturnTheAesKeyWhenCalledWithTheEncodedByteArray() { + //given + final SecretKey expected = EncryptionKeyUtil.generateAesKey(); + + //when + final SecretKey actual = EncryptionKeyUtil.byteArrayToAesKey(expected.getEncoded()); + + //then + Assertions.assertEquals(expected, actual); + } + + @Test + void testByteArrayToRsaPublicKeyShouldReturnThePublicKeyWhenCalledWithTheEncodedByteArray() { + //given + final KeyPair keyPair = EncryptionKeyUtil.generateRsaKeyPair(); + + //when + final PublicKey actual = EncryptionKeyUtil.byteArrayToRsaPublicKey(keyPair.getPublic().getEncoded()); + + //then + Assertions.assertEquals(keyPair.getPublic(), actual); + } + + @Test + void testByteArrayToRsaPublicKeyShouldThrowExceptionWhenCalledWithNull() { + //given + + //when + Assertions.assertThrows(CryptoException.class, () -> EncryptionKeyUtil.byteArrayToRsaPublicKey(null)); + + //then + exception + } +} diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadataTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadataTest.java new file mode 100644 index 0000000..5c47d51 --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/ArchivedFileMetadataTest.java @@ -0,0 +1,59 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.filebarj.core.crypto.EncryptionKeyUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.UUID; + +class ArchivedFileMetadataTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfFullyPopulatedObject() throws JsonProcessingException { + //given + final ArchivedFileMetadata expected = ArchivedFileMetadata.builder() + .id(UUID.randomUUID()) + .originalChecksum("checksum") + .archivedChecksum("archived") + .archiveLocation(ArchiveEntryLocator.builder() + .backupIncrement(1) + .entryName(UUID.randomUUID()) + .randomBytes(EncryptionKeyUtil.generateSecureRandomBytes()) + .build()) + .files(Set.of(UUID.randomUUID())) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final ArchivedFileMetadata actual = objectMapper.readerFor(ArchivedFileMetadata.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfMinimalObject() throws JsonProcessingException { + //given + final ArchivedFileMetadata expected = ArchivedFileMetadata.builder() + .id(UUID.randomUUID()) + .archiveLocation(ArchiveEntryLocator.builder() + .backupIncrement(1) + .entryName(UUID.randomUUID()) + .build()) + .files(Set.of(UUID.randomUUID())) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final ArchivedFileMetadata actual = objectMapper.readerFor(ArchivedFileMetadata.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } +} diff --git a/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/FileMetadataTest.java b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/FileMetadataTest.java new file mode 100644 index 0000000..3a4adab --- /dev/null +++ b/file-barj-core/src/test/java/com/github/nagyesta/filebarj/core/model/FileMetadataTest.java @@ -0,0 +1,63 @@ +package com.github.nagyesta.filebarj.core.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.nagyesta.filebarj.core.model.enums.Change; +import com.github.nagyesta.filebarj.core.model.enums.FileType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.UUID; + +class FileMetadataTest { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfFullyPopulatedObject() throws JsonProcessingException { + //given + final FileMetadata expected = FileMetadata.builder() + .id(UUID.randomUUID()) + .absolutePath(Path.of("test", "file", ".path.txt").toAbsolutePath()) + .archiveMetadataId(UUID.randomUUID()) + .fileType(FileType.REGULAR_FILE) + .owner("owner") + .group("group") + .posixPermissions("rwxr-xr-x") + .originalSizeBytes(1024L) + .lastModifiedUtcEpochSeconds(123L) + .originalChecksum("checksum") + .hidden(true) + .status(Change.NEW) + .error("error") + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final FileMetadata actual = objectMapper.readerFor(FileMetadata.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } + + @Test + void testDeserializeShouldRecreatePreviousStateWhenCalledOnSerializedStateOfMinimalObject() throws JsonProcessingException { + //given + final FileMetadata expected = FileMetadata.builder() + .id(UUID.randomUUID()) + .absolutePath(Path.of("test", "file", "missing.md").toAbsolutePath()) + .fileType(FileType.SYMBOLIC_LINK) + .status(Change.DELETED) + .build(); + final String json = objectMapper.writer().writeValueAsString(expected); + + //when + final FileMetadata actual = objectMapper.readerFor(FileMetadata.class).readValue(json); + + //then + Assertions.assertEquals(expected, actual); + Assertions.assertEquals(expected.hashCode(), actual.hashCode()); + } +} diff --git a/file-barj-core/src/test/resources/.gitkeep b/file-barj-core/src/test/resources/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/file-barj-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/file-barj-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 0000000..058d589 --- /dev/null +++ b/file-barj-core/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +com.github.nagyesta.abortmission.booster.jupiter.extension.AbortMissionExtension diff --git a/file-barj-core/src/test/resources/junit-platform.properties b/file-barj-core/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..3377e75 --- /dev/null +++ b/file-barj-core/src/test/resources/junit-platform.properties @@ -0,0 +1,4 @@ +junit.jupiter.extensions.autodetection.enabled=true +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.mode.default=concurrent +junit.jupiter.execution.parallel.mode.classes.default=concurrent diff --git a/file-barj-job/build.gradle.kts b/file-barj-job/build.gradle.kts index a004f18..2e07483 100644 --- a/file-barj-job/build.gradle.kts +++ b/file-barj-job/build.gradle.kts @@ -2,15 +2,18 @@ plugins { id("java") } -repositories { - mavenCentral() -} - dependencies { - testImplementation(platform("org.junit:junit-bom:5.9.1")) - testImplementation("org.junit.jupiter:junit-jupiter") + implementation(project(":file-barj-core")) + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.jupiter) + testImplementation(libs.abort.mission.jupiter) + testImplementation(libs.mockito.core) } tasks.test { useJUnitPlatform() } + +abortMission { + toolVersion = libs.versions.abortMission.get() +} diff --git a/file-barj-job/lombok.config b/file-barj-job/lombok.config new file mode 100644 index 0000000..8a1cf95 --- /dev/null +++ b/file-barj-job/lombok.config @@ -0,0 +1,4 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true +lombok.nonNull.exceptionType = IllegalArgumentException diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a622e25..8023753 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,11 @@ [versions] -logback = "1.4.11" -bouncycastle = "1.76" -hibernateValidator = "8.0.1.Final" -findbugs = "3.0.2" -lombok = "1.18.28" +#logback = "1.4.11" +#bouncycastle = "1.76" +#hibernateValidator = "8.0.1.Final" commonsCodec = "1.16.0" +commonsCompress = "1.24.0" +commonsCrypto = "1.2.0" +commonsIo = "2.13.0" mockitoCore = "5.5.0" jupiter = "5.10.0" abortMission = "4.2.57" @@ -20,22 +21,22 @@ gitVersionerPlugin = "1.6.7" owaspPlugin = "8.4.0" [libraries] -logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } +#logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +#logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" } -bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } +#bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } -hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernateValidator" } - -findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs" } +#hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernateValidator" } +commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" } +commons-crypto = { module = "org.apache.commons:commons-crypto", version.ref = "commonsCrypto" } commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" } +commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } -lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } - jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jupiter" } +junit-bom = { module = "org.junit:junit-bom", version.ref = "jupiter" } abort-mission-jupiter = { module = "com.github.nagyesta.abort-mission.boosters:abort.booster-junit-jupiter", version.ref = "abortMission" } @@ -44,11 +45,10 @@ jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-dataformat-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" } -jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } [bundles] -logback = ["logback-classic", "logback-core"] -jackson = ["jackson-core", "jackson-annotations", "jackson-databind", "jackson-dataformat-xml", "jackson-datatype-jsr310"] +#logback = ["logback-classic", "logback-core"] +jackson = ["jackson-core", "jackson-annotations", "jackson-databind", "jackson-dataformat-xml"] [plugins] lombok = { id = "io.freefair.lombok", version.ref = "lombokPlugin" }