diff --git a/src/main/java/me/itzg/helpers/sync/Sync.java b/src/main/java/me/itzg/helpers/sync/Sync.java index 6acbcd2b..22fc8dd2 100644 --- a/src/main/java/me/itzg/helpers/sync/Sync.java +++ b/src/main/java/me/itzg/helpers/sync/Sync.java @@ -1,18 +1,16 @@ package me.itzg.helpers.sync; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Callable; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.McImageHelper; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.Callable; - @Command(name = "sync", description = "Synchronizes the contents of one directory to another.") @ToString @@ -34,24 +32,14 @@ public class Sync implements Callable { @ToString.Exclude List extra; - @Parameters(index = "0", description = "source directory") - Path src; - - @Parameters(index = "1", description = "destination directory") - Path dest; + @Parameters(arity = "2..*", description = "src... dest directories", + split = McImageHelper.SPLIT_COMMA_NL, splitSynopsisLabel = McImageHelper.SPLIT_SYNOPSIS_COMMA_NL) + List srcDest; @Override public Integer call() throws Exception { log.debug("Configured with {}", this); - try { - Files.walkFileTree(src, new SynchronizingFileVisitor(src, dest, skipNewerInDestination, new CopyingFileProcessor())); - } catch (IOException e) { - log.error("Failed to sync {} into {} : {}", src, dest, e.getMessage()); - log.debug("Details", e); - return 1; - } - - return 0; + return SynchronizingFileVisitor.walkDirectories(srcDest, skipNewerInDestination, new CopyingFileProcessor()); } } diff --git a/src/main/java/me/itzg/helpers/sync/SyncAndInterpolate.java b/src/main/java/me/itzg/helpers/sync/SyncAndInterpolate.java index 9cc11562..dfaae4b8 100644 --- a/src/main/java/me/itzg/helpers/sync/SyncAndInterpolate.java +++ b/src/main/java/me/itzg/helpers/sync/SyncAndInterpolate.java @@ -1,7 +1,11 @@ package me.itzg.helpers.sync; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Callable; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.McImageHelper; import me.itzg.helpers.env.Interpolator; import me.itzg.helpers.env.StandardEnvironmentVariablesProvider; import picocli.CommandLine.ArgGroup; @@ -9,11 +13,6 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.Callable; - @Command(name = "sync-and-interpolate", description = "Synchronizes the contents of one directory to another with conditional variable interpolation.") @ToString @@ -31,34 +30,21 @@ public class SyncAndInterpolate implements Callable { @ArgGroup(multiplicity = "1", exclusive = false) ReplaceEnvOptions replaceEnv = new ReplaceEnvOptions(); - @Parameters(index = "0", description = "source directory") - Path src; - - @Parameters(index = "1", description = "destination directory") - Path dest; + @Parameters(arity = "2..*", description = "src... dest directories", + split = McImageHelper.SPLIT_COMMA_NL, splitSynopsisLabel = McImageHelper.SPLIT_SYNOPSIS_COMMA_NL) + List srcDest; @Override public Integer call() throws Exception { log.debug("Configured with {}", this); - try { - Files.walkFileTree(src, - new SynchronizingFileVisitor(src, dest, skipNewerInDestination, - new InterpolatingFileProcessor( - replaceEnv, - new Interpolator(new StandardEnvironmentVariablesProvider(), replaceEnv.prefix), - new CopyingFileProcessor() - ) - - ) - ); - } catch (IOException e) { - log.error("Failed to sync and interpolate {} into {} : {}", src, dest, e.getMessage()); - log.debug("Details", e); - return 1; - } + final InterpolatingFileProcessor fileProcessor = new InterpolatingFileProcessor( + replaceEnv, + new Interpolator(new StandardEnvironmentVariablesProvider(), replaceEnv.prefix), + new CopyingFileProcessor() + ); - return 0; + return SynchronizingFileVisitor.walkDirectories(srcDest, skipNewerInDestination, fileProcessor); } } diff --git a/src/main/java/me/itzg/helpers/sync/SynchronizingFileVisitor.java b/src/main/java/me/itzg/helpers/sync/SynchronizingFileVisitor.java index 73633cfb..27ccb87d 100644 --- a/src/main/java/me/itzg/helpers/sync/SynchronizingFileVisitor.java +++ b/src/main/java/me/itzg/helpers/sync/SynchronizingFileVisitor.java @@ -1,7 +1,5 @@ package me.itzg.helpers.sync; -import lombok.extern.slf4j.Slf4j; - import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; @@ -9,6 +7,8 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.util.List; +import lombok.extern.slf4j.Slf4j; @Slf4j class SynchronizingFileVisitor implements FileVisitor { @@ -24,6 +24,24 @@ public SynchronizingFileVisitor(Path src, Path dest, boolean skipNewerInDestinat this.fileProcessor = fileProcessor; } + static int walkDirectories(List srcDest, boolean skipNewerInDestination, + FileProcessor fileProcessor + ) { + final Path dest = srcDest.getLast(); + + for (final Path src : srcDest.subList(0, srcDest.size() - 1)) { + try { + Files.walkFileTree(src, new SynchronizingFileVisitor(src, dest, skipNewerInDestination, fileProcessor)); + } catch (IOException e) { + log.error("Failed to sync and interpolate {} into {} : {}", src, dest, e.getMessage()); + log.debug("Details", e); + return 1; + } + } + + return 0; + } + @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { log.trace("pre visit dir={}", dir); @@ -79,9 +97,9 @@ private boolean shouldProcessFile(Path srcFile, Path destFile) throws IOExceptio } @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) { - log.warn("Failed to access {} due to {}", file, exc.getMessage()); - log.debug("Details", exc); + public FileVisitResult visitFileFailed(Path file, IOException e) { + log.warn("Failed to visit file {} due to {}:{}", file, e.getClass(), e.getMessage()); + log.debug("Details", e); return FileVisitResult.CONTINUE; } diff --git a/src/test/java/me/itzg/helpers/sync/SyncAndInterpolateTest.java b/src/test/java/me/itzg/helpers/sync/SyncAndInterpolateTest.java new file mode 100644 index 00000000..d778bd95 --- /dev/null +++ b/src/test/java/me/itzg/helpers/sync/SyncAndInterpolateTest.java @@ -0,0 +1,106 @@ +package me.itzg.helpers.sync; + +import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import picocli.CommandLine; + +class SyncAndInterpolateTest { + + @Nested + class NonInterpolatedScenarios { + @ParameterizedTest + @ValueSource(classes = {Sync.class, SyncAndInterpolate.class}) + void copiesFromOneSrc(Class commandClass, @TempDir Path tempDir) throws Exception { + final Path srcDir = Files.createDirectory(tempDir.resolve("src")); + Files.createFile(srcDir.resolve("test1.txt")); + Files.createFile(srcDir.resolve("test2.txt")); + final Path destDir = Files.createDirectory(tempDir.resolve("dest")); + + final String stderr = tapSystemErr(() -> { + final int exitCode = new CommandLine(commandClass) + .execute( + "--replace-env-file-suffixes=json", + srcDir.toString(), + destDir.toString() + ); + + assertThat(exitCode).isEqualTo(0); + }); + assertThat(stderr).isBlank(); + + assertThat(destDir).isNotEmptyDirectory(); + assertThat(destDir.resolve("test1.txt")).exists(); + assertThat(destDir.resolve("test2.txt")).exists(); + } + + @ParameterizedTest + @ValueSource(classes = {Sync.class, SyncAndInterpolate.class}) + void copiesFromTwoSrc(Class commandClass, @TempDir Path tempDir) throws Exception { + final Path srcDir1 = Files.createDirectory(tempDir.resolve("src1")); + Files.createFile(srcDir1.resolve("test1.txt")); + Files.createFile(srcDir1.resolve("test2.txt")); + final Path srcDir2 = Files.createDirectory(tempDir.resolve("src2")); + Files.createFile(srcDir2.resolve("test3.txt")); + Files.createFile(srcDir2.resolve("test4.txt")); + final Path destDir = Files.createDirectory(tempDir.resolve("dest")); + + final String stderr = tapSystemErr(() -> { + final int exitCode = new CommandLine(commandClass) + .execute( + "--replace-env-file-suffixes=json", + srcDir1.toString(), + srcDir2.toString(), + destDir.toString() + ); + + assertThat(exitCode).isEqualTo(0); + }); + assertThat(stderr).isBlank(); + + assertThat(destDir).isNotEmptyDirectory(); + assertThat(destDir.resolve("test1.txt")).exists(); + assertThat(destDir.resolve("test2.txt")).exists(); + assertThat(destDir.resolve("test3.txt")).exists(); + assertThat(destDir.resolve("test4.txt")).exists(); + } + + @ParameterizedTest + @ValueSource(classes = {Sync.class, SyncAndInterpolate.class}) + void copiesFromTwoSrcCommaDelim(Class commandClass, @TempDir Path tempDir) throws Exception { + final Path srcDir1 = Files.createDirectory(tempDir.resolve("src1")); + Files.createFile(srcDir1.resolve("test1.txt")); + Files.createFile(srcDir1.resolve("test2.txt")); + final Path srcDir2 = Files.createDirectory(tempDir.resolve("src2")); + Files.createFile(srcDir2.resolve("test3.txt")); + Files.createFile(srcDir2.resolve("test4.txt")); + final Path destDir = Files.createDirectory(tempDir.resolve("dest")); + + final String stderr = tapSystemErr(() -> { + final int exitCode = new CommandLine(commandClass) + .execute( + "--replace-env-file-suffixes=json", + String.join(",", srcDir1.toString(), srcDir2.toString()), + destDir.toString() + ); + + assertThat(exitCode).isEqualTo(0); + }); + assertThat(stderr).isBlank(); + + assertThat(destDir).isNotEmptyDirectory(); + assertThat(destDir.resolve("test1.txt")).exists(); + assertThat(destDir.resolve("test2.txt")).exists(); + assertThat(destDir.resolve("test3.txt")).exists(); + assertThat(destDir.resolve("test4.txt")).exists(); + } + + } + +} \ No newline at end of file