Skip to content

Commit 8ba3a6b

Browse files
committed
sync: allow multiple source directories
1 parent bf0818b commit 8ba3a6b

File tree

4 files changed

+150
-52
lines changed

4 files changed

+150
-52
lines changed
Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package me.itzg.helpers.sync;
22

3+
import java.nio.file.Path;
4+
import java.util.List;
5+
import java.util.concurrent.Callable;
36
import lombok.ToString;
47
import lombok.extern.slf4j.Slf4j;
8+
import me.itzg.helpers.McImageHelper;
59
import picocli.CommandLine;
610
import picocli.CommandLine.Command;
711
import picocli.CommandLine.Option;
812
import picocli.CommandLine.Parameters;
913

10-
import java.io.IOException;
11-
import java.nio.file.Files;
12-
import java.nio.file.Path;
13-
import java.util.List;
14-
import java.util.concurrent.Callable;
15-
1614
@Command(name = "sync",
1715
description = "Synchronizes the contents of one directory to another.")
1816
@ToString
@@ -34,24 +32,14 @@ public class Sync implements Callable<Integer> {
3432
@ToString.Exclude
3533
List<String> extra;
3634

37-
@Parameters(index = "0", description = "source directory")
38-
Path src;
39-
40-
@Parameters(index = "1", description = "destination directory")
41-
Path dest;
35+
@Parameters(arity = "2..*", description = "src... dest directories",
36+
split = McImageHelper.SPLIT_COMMA_NL, splitSynopsisLabel = McImageHelper.SPLIT_SYNOPSIS_COMMA_NL)
37+
List<Path> srcDest;
4238

4339
@Override
4440
public Integer call() throws Exception {
4541
log.debug("Configured with {}", this);
4642

47-
try {
48-
Files.walkFileTree(src, new SynchronizingFileVisitor(src, dest, skipNewerInDestination, new CopyingFileProcessor()));
49-
} catch (IOException e) {
50-
log.error("Failed to sync {} into {} : {}", src, dest, e.getMessage());
51-
log.debug("Details", e);
52-
return 1;
53-
}
54-
55-
return 0;
43+
return SynchronizingFileVisitor.walkDirectories(srcDest, skipNewerInDestination, new CopyingFileProcessor());
5644
}
5745
}
Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package me.itzg.helpers.sync;
22

3+
import java.nio.file.Path;
4+
import java.util.List;
5+
import java.util.concurrent.Callable;
36
import lombok.ToString;
47
import lombok.extern.slf4j.Slf4j;
8+
import me.itzg.helpers.McImageHelper;
59
import me.itzg.helpers.env.Interpolator;
610
import me.itzg.helpers.env.StandardEnvironmentVariablesProvider;
711
import picocli.CommandLine.ArgGroup;
812
import picocli.CommandLine.Command;
913
import picocli.CommandLine.Option;
1014
import picocli.CommandLine.Parameters;
1115

12-
import java.io.IOException;
13-
import java.nio.file.Files;
14-
import java.nio.file.Path;
15-
import java.util.concurrent.Callable;
16-
1716
@Command(name = "sync-and-interpolate",
1817
description = "Synchronizes the contents of one directory to another with conditional variable interpolation.")
1918
@ToString
@@ -31,34 +30,21 @@ public class SyncAndInterpolate implements Callable<Integer> {
3130
@ArgGroup(multiplicity = "1", exclusive = false)
3231
ReplaceEnvOptions replaceEnv = new ReplaceEnvOptions();
3332

34-
@Parameters(index = "0", description = "source directory")
35-
Path src;
36-
37-
@Parameters(index = "1", description = "destination directory")
38-
Path dest;
33+
@Parameters(arity = "2..*", description = "src... dest directories",
34+
split = McImageHelper.SPLIT_COMMA_NL, splitSynopsisLabel = McImageHelper.SPLIT_SYNOPSIS_COMMA_NL)
35+
List<Path> srcDest;
3936

4037
@Override
4138
public Integer call() throws Exception {
4239
log.debug("Configured with {}", this);
4340

44-
try {
45-
Files.walkFileTree(src,
46-
new SynchronizingFileVisitor(src, dest, skipNewerInDestination,
47-
new InterpolatingFileProcessor(
48-
replaceEnv,
49-
new Interpolator(new StandardEnvironmentVariablesProvider(), replaceEnv.prefix),
50-
new CopyingFileProcessor()
51-
)
52-
53-
)
54-
);
55-
} catch (IOException e) {
56-
log.error("Failed to sync and interpolate {} into {} : {}", src, dest, e.getMessage());
57-
log.debug("Details", e);
58-
return 1;
59-
}
41+
final InterpolatingFileProcessor fileProcessor = new InterpolatingFileProcessor(
42+
replaceEnv,
43+
new Interpolator(new StandardEnvironmentVariablesProvider(), replaceEnv.prefix),
44+
new CopyingFileProcessor()
45+
);
6046

61-
return 0;
47+
return SynchronizingFileVisitor.walkDirectories(srcDest, skipNewerInDestination, fileProcessor);
6248
}
6349

6450
}

src/main/java/me/itzg/helpers/sync/SynchronizingFileVisitor.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package me.itzg.helpers.sync;
22

3-
import lombok.extern.slf4j.Slf4j;
4-
53
import java.io.IOException;
64
import java.nio.file.FileVisitResult;
75
import java.nio.file.FileVisitor;
86
import java.nio.file.Files;
97
import java.nio.file.Path;
108
import java.nio.file.attribute.BasicFileAttributes;
119
import java.nio.file.attribute.FileTime;
10+
import java.util.List;
11+
import lombok.extern.slf4j.Slf4j;
1212

1313
@Slf4j
1414
class SynchronizingFileVisitor implements FileVisitor<Path> {
@@ -24,6 +24,24 @@ public SynchronizingFileVisitor(Path src, Path dest, boolean skipNewerInDestinat
2424
this.fileProcessor = fileProcessor;
2525
}
2626

27+
static int walkDirectories(List<Path> srcDest, boolean skipNewerInDestination,
28+
FileProcessor fileProcessor
29+
) {
30+
final Path dest = srcDest.getLast();
31+
32+
for (final Path src : srcDest.subList(0, srcDest.size() - 1)) {
33+
try {
34+
Files.walkFileTree(src, new SynchronizingFileVisitor(src, dest, skipNewerInDestination, fileProcessor));
35+
} catch (IOException e) {
36+
log.error("Failed to sync and interpolate {} into {} : {}", src, dest, e.getMessage());
37+
log.debug("Details", e);
38+
return 1;
39+
}
40+
}
41+
42+
return 0;
43+
}
44+
2745
@Override
2846
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
2947
log.trace("pre visit dir={}", dir);
@@ -79,9 +97,9 @@ private boolean shouldProcessFile(Path srcFile, Path destFile) throws IOExceptio
7997
}
8098

8199
@Override
82-
public FileVisitResult visitFileFailed(Path file, IOException exc) {
83-
log.warn("Failed to access {} due to {}", file, exc.getMessage());
84-
log.debug("Details", exc);
100+
public FileVisitResult visitFileFailed(Path file, IOException e) {
101+
log.warn("Failed to visit file {} due to {}:{}", file, e.getClass(), e.getMessage());
102+
log.debug("Details", e);
85103
return FileVisitResult.CONTINUE;
86104
}
87105

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package me.itzg.helpers.sync;
2+
3+
import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import org.junit.jupiter.api.Nested;
9+
import org.junit.jupiter.api.io.TempDir;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.ValueSource;
12+
import picocli.CommandLine;
13+
14+
class SyncAndInterpolateTest {
15+
16+
@Nested
17+
class NonInterpolatedScenarios {
18+
@ParameterizedTest
19+
@ValueSource(classes = {Sync.class, SyncAndInterpolate.class})
20+
void copiesFromOneSrc(Class<?> commandClass, @TempDir Path tempDir) throws Exception {
21+
final Path srcDir = Files.createDirectory(tempDir.resolve("src"));
22+
Files.createFile(srcDir.resolve("test1.txt"));
23+
Files.createFile(srcDir.resolve("test2.txt"));
24+
final Path destDir = Files.createDirectory(tempDir.resolve("dest"));
25+
26+
final String stderr = tapSystemErr(() -> {
27+
final int exitCode = new CommandLine(commandClass)
28+
.execute(
29+
"--replace-env-file-suffixes=json",
30+
srcDir.toString(),
31+
destDir.toString()
32+
);
33+
34+
assertThat(exitCode).isEqualTo(0);
35+
});
36+
assertThat(stderr).isBlank();
37+
38+
assertThat(destDir).isNotEmptyDirectory();
39+
assertThat(destDir.resolve("test1.txt")).exists();
40+
assertThat(destDir.resolve("test2.txt")).exists();
41+
}
42+
43+
@ParameterizedTest
44+
@ValueSource(classes = {Sync.class, SyncAndInterpolate.class})
45+
void copiesFromTwoSrc(Class<?> commandClass, @TempDir Path tempDir) throws Exception {
46+
final Path srcDir1 = Files.createDirectory(tempDir.resolve("src1"));
47+
Files.createFile(srcDir1.resolve("test1.txt"));
48+
Files.createFile(srcDir1.resolve("test2.txt"));
49+
final Path srcDir2 = Files.createDirectory(tempDir.resolve("src2"));
50+
Files.createFile(srcDir2.resolve("test3.txt"));
51+
Files.createFile(srcDir2.resolve("test4.txt"));
52+
final Path destDir = Files.createDirectory(tempDir.resolve("dest"));
53+
54+
final String stderr = tapSystemErr(() -> {
55+
final int exitCode = new CommandLine(commandClass)
56+
.execute(
57+
"--replace-env-file-suffixes=json",
58+
srcDir1.toString(),
59+
srcDir2.toString(),
60+
destDir.toString()
61+
);
62+
63+
assertThat(exitCode).isEqualTo(0);
64+
});
65+
assertThat(stderr).isBlank();
66+
67+
assertThat(destDir).isNotEmptyDirectory();
68+
assertThat(destDir.resolve("test1.txt")).exists();
69+
assertThat(destDir.resolve("test2.txt")).exists();
70+
assertThat(destDir.resolve("test3.txt")).exists();
71+
assertThat(destDir.resolve("test4.txt")).exists();
72+
}
73+
74+
@ParameterizedTest
75+
@ValueSource(classes = {Sync.class, SyncAndInterpolate.class})
76+
void copiesFromTwoSrcCommaDelim(Class<?> commandClass, @TempDir Path tempDir) throws Exception {
77+
final Path srcDir1 = Files.createDirectory(tempDir.resolve("src1"));
78+
Files.createFile(srcDir1.resolve("test1.txt"));
79+
Files.createFile(srcDir1.resolve("test2.txt"));
80+
final Path srcDir2 = Files.createDirectory(tempDir.resolve("src2"));
81+
Files.createFile(srcDir2.resolve("test3.txt"));
82+
Files.createFile(srcDir2.resolve("test4.txt"));
83+
final Path destDir = Files.createDirectory(tempDir.resolve("dest"));
84+
85+
final String stderr = tapSystemErr(() -> {
86+
final int exitCode = new CommandLine(commandClass)
87+
.execute(
88+
"--replace-env-file-suffixes=json",
89+
String.join(",", srcDir1.toString(), srcDir2.toString()),
90+
destDir.toString()
91+
);
92+
93+
assertThat(exitCode).isEqualTo(0);
94+
});
95+
assertThat(stderr).isBlank();
96+
97+
assertThat(destDir).isNotEmptyDirectory();
98+
assertThat(destDir.resolve("test1.txt")).exists();
99+
assertThat(destDir.resolve("test2.txt")).exists();
100+
assertThat(destDir.resolve("test3.txt")).exists();
101+
assertThat(destDir.resolve("test4.txt")).exists();
102+
}
103+
104+
}
105+
106+
}

0 commit comments

Comments
 (0)