1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.buildcache.its;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.nio.charset.StandardCharsets;
24 import java.nio.file.Files;
25 import java.nio.file.LinkOption;
26 import java.nio.file.Path;
27 import java.nio.file.Paths;
28 import java.nio.file.attribute.FileTime;
29 import java.nio.file.attribute.PosixFilePermission;
30 import java.nio.file.attribute.PosixFilePermissions;
31 import java.time.Instant;
32 import java.time.ZoneId;
33 import java.time.ZonedDateTime;
34 import java.time.format.DateTimeFormatter;
35 import java.util.Arrays;
36 import java.util.EnumSet;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.Set;
40 import java.util.TreeMap;
41 import java.util.function.Consumer;
42 import java.util.function.Predicate;
43 import java.util.regex.Pattern;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46
47 import org.apache.maven.buildcache.its.junit.BeforeEach;
48 import org.apache.maven.buildcache.its.junit.Inject;
49 import org.apache.maven.buildcache.its.junit.IntegrationTest;
50 import org.apache.maven.buildcache.its.junit.IntegrationTestExtension;
51 import org.apache.maven.it.VerificationException;
52 import org.apache.maven.it.Verifier;
53 import org.jetbrains.annotations.NotNull;
54 import org.junit.jupiter.api.AfterEach;
55 import org.junit.jupiter.params.ParameterizedTest;
56 import org.junit.jupiter.params.provider.Arguments;
57 import org.junit.jupiter.params.provider.MethodSource;
58 import org.testcontainers.containers.GenericContainer;
59 import org.testcontainers.junit.jupiter.Container;
60 import org.testcontainers.junit.jupiter.Testcontainers;
61 import org.testcontainers.utility.DockerImageName;
62
63 import static org.apache.maven.buildcache.xml.CacheConfigImpl.REMOTE_SERVER_ID_PROPERTY_NAME;
64 import static org.apache.maven.buildcache.xml.CacheConfigImpl.REMOTE_URL_PROPERTY_NAME;
65 import static org.junit.jupiter.api.Assertions.assertFalse;
66 import static org.junit.jupiter.api.Assertions.assertTrue;
67
68 @IntegrationTest("src/test/projects/remote-cache-dav")
69 @Testcontainers(disabledWithoutDocker = true)
70 public class RemoteCacheDavTest {
71
72 public static final String DAV_DOCKER_IMAGE =
73 "xama/nginx-webdav@sha256:84171a7e67d7e98eeaa67de58e3ce141ec1d0ee9c37004e7096698c8379fd9cf";
74 private static final String DAV_USERNAME = "admin";
75 private static final String DAV_PASSWORD = "admin";
76 private static final String REPO_ID = "build-cache";
77 private static final String HTTP_TRANSPORT_PRIORITY =
78 "aether.priority.org.eclipse.aether.transport.http.HttpTransporterFactory";
79 private static final String WAGON_TRANSPORT_PRIORITY =
80 "aether.priority.org.eclipse.aether.transport.wagon.WagonTransporterFactory";
81 private static final String MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED = "maven.build.cache.remote.save.enabled";
82
83 @Container
84 GenericContainer<?> dav;
85
86 @Inject
87 Verifier verifier;
88
89 Path basedir;
90 Path remoteCache;
91 Path localCache;
92 Path settings;
93 Path logDir;
94
95 @BeforeEach
96 void setup() throws IOException {
97 basedir = Paths.get(verifier.getBasedir());
98 remoteCache = basedir.resolveSibling("cache-remote").toAbsolutePath().normalize();
99 localCache = basedir.resolveSibling("cache-local").toAbsolutePath().normalize();
100 settings = basedir.resolve("../settings.xml").toAbsolutePath().normalize();
101 logDir = basedir.getParent();
102
103 Files.createDirectories(remoteCache);
104
105 Files.write(
106 settings,
107 ("<settings>"
108 + "<servers><server>"
109 + "<id>" + REPO_ID + "</id>"
110 + "<username>" + DAV_USERNAME + "</username>"
111 + "<password>" + DAV_PASSWORD + "</password>"
112 + "</server></servers></settings>")
113 .getBytes());
114
115 dav = new GenericContainer<>(DockerImageName.parse(DAV_DOCKER_IMAGE))
116 .withReuse(false)
117 .withExposedPorts(80)
118 .withEnv("WEBDAV_USERNAME", DAV_USERNAME)
119 .withEnv("WEBDAV_PASSWORD", DAV_PASSWORD)
120 .withFileSystemBind(remoteCache.toString(), "/var/webdav/public");
121 }
122
123 @AfterEach
124 public void cleanup() throws Exception {
125 dav.execInContainer("rm", "-rf", "/var/webdav");
126 cleanDirs(localCache);
127 dav.close();
128 }
129
130 public static Stream<Arguments> transports() {
131 return Stream.of(Arguments.of("wagon"), Arguments.of("http"));
132 }
133
134 @ParameterizedTest
135 @MethodSource("transports")
136 public void doTestRemoteCache(String transport) throws VerificationException, IOException {
137 String url =
138 ("wagon".equals(transport) ? "dav:" : "") + "http://localhost:" + dav.getFirstMappedPort() + "/mbce";
139 substitute(
140 basedir.resolve(".mvn/maven-build-cache-config.xml"),
141 "url",
142 url,
143 "id",
144 REPO_ID,
145 "location",
146 localCache.toString());
147
148 verifier.setAutoclean(false);
149
150 cleanDirs(localCache, remoteCache.resolve("mbce"));
151 assertFalse(hasBuildInfoXml(localCache), () -> error(localCache, "local", false));
152 assertFalse(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", false));
153
154 verifier.getCliOptions().clear();
155 verifier.addCliOption("--settings=" + settings);
156 verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
157 verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
158 verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=false");
159 verifier.setLogFileName("../log-1.txt");
160 verifier.executeGoals(Arrays.asList("clean", "install"));
161 verifier.verifyErrorFreeLog();
162
163 assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
164 assertFalse(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", false));
165
166 cleanDirs(localCache);
167
168 verifier.getCliOptions().clear();
169 verifier.addCliOption("--settings=" + settings);
170 if (!"wagon".equals(transport)) {
171 verifier.setSystemProperty("aether.connector.http.supportWebDav", "true");
172 }
173 verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
174 verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
175 verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=true");
176 verifier.setLogFileName("../log-2.txt");
177 verifier.executeGoals(Arrays.asList("clean", "install"));
178 verifier.verifyErrorFreeLog();
179
180 assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
181 assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
182
183 cleanDirs(localCache);
184
185 verifier.getCliOptions().clear();
186 verifier.addCliOption("--settings=" + settings);
187 if (!"wagon".equals(transport)) {
188 verifier.setSystemProperty("aether.connector.http.supportWebDav", "true");
189 }
190 verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
191 verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
192 verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=false");
193 verifier.setLogFileName("../log-3.txt");
194 verifier.executeGoals(Arrays.asList("clean", "install"));
195 verifier.verifyErrorFreeLog();
196
197 assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
198 assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
199
200
201 substitute(
202 basedir.resolve(".mvn/maven-build-cache-config.xml"),
203 "url",
204 "http://foo.com",
205 "id",
206 "foo",
207 "location",
208 localCache.toString());
209
210 cleanDirs(localCache);
211 try {
212
213
214 dav.execInContainer("rm", "-rf", "/var/webdav/public/*");
215 } catch (InterruptedException e) {
216 throw new IOException("cannot delete remote cache");
217 }
218
219 verifier.getCliOptions().clear();
220 verifier.addCliOption("--settings=" + settings);
221 verifier.addCliOption("-X");
222 verifier.addCliOption("-D" + HTTP_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "0" : "10"));
223 verifier.addCliOption("-D" + WAGON_TRANSPORT_PRIORITY + "=" + ("wagon".equals(transport) ? "10" : "0"));
224 verifier.addCliOption("-D" + MAVEN_BUILD_CACHE_REMOTE_SAVE_ENABLED + "=true");
225 verifier.setSystemProperty(REMOTE_URL_PROPERTY_NAME, url);
226 verifier.setSystemProperty(REMOTE_SERVER_ID_PROPERTY_NAME, REPO_ID);
227 verifier.setLogFileName("../log-4.txt");
228 verifier.executeGoals(Arrays.asList("clean", "install"));
229 verifier.verifyErrorFreeLog();
230
231 assertTrue(hasBuildInfoXml(localCache), () -> error(localCache, "local", true));
232 assertTrue(hasBuildInfoXml(remoteCache), () -> error(remoteCache, "remote", true));
233 }
234
235 private boolean hasBuildInfoXml(Path cache) throws IOException {
236 return Files.walk(cache).anyMatch(isBuildInfoXml());
237 }
238
239 @NotNull
240 private Predicate<Path> isBuildInfoXml() {
241 return p -> p.getFileName().toString().equals("buildinfo.xml");
242 }
243
244 private void cleanDirs(Path... paths) throws IOException {
245 for (Path path : paths) {
246 IntegrationTestExtension.deleteDir(path);
247 Files.createDirectories(path);
248 Runtime.getRuntime().exec("chmod go+rwx " + path);
249 }
250 }
251
252 private static void substitute(Path path, String... strings) throws IOException {
253 String str = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
254 for (int i = 0; i < strings.length / 2; i++) {
255 str = str.replaceAll(Pattern.quote("${" + strings[i * 2] + "}"), strings[i * 2 + 1]);
256 }
257 Files.deleteIfExists(path);
258 Files.write(path, str.getBytes(StandardCharsets.UTF_8));
259 }
260
261 private String error(Path directory, String cache, boolean shouldHave) {
262 StringBuilder sb =
263 new StringBuilder("The " + cache + " cache should " + (shouldHave ? "" : "not ") + "contain a build\n");
264 try {
265 sb.append("Contents:\n");
266 Files.walk(directory).forEach(p -> sb.append(" ").append(p).append("\n"));
267
268 for (Path log : Files.list(logDir)
269 .filter(p -> p.getFileName().toString().matches("log.*\\.txt"))
270 .collect(Collectors.toList())) {
271 sb.append("Log file: ").append(log).append("\n");
272 Files.lines(log).forEach(l -> sb.append(" ").append(l).append("\n"));
273 }
274
275 sb.append("Container log:\n");
276 Stream.of(dav.getLogs().split("\n"))
277 .forEach(l -> sb.append(" ").append(l).append("\n"));
278
279 sb.append("Remote cache listing:\n");
280 ls(remoteCache, s -> sb.append(" ").append(s).append("\n"));
281 } catch (IOException e) {
282 sb.append("Error: ").append(e);
283 }
284 return sb.toString();
285 }
286
287 private static void ls(Path currentDir, Consumer<String> out) throws IOException {
288 class PathEntry implements Comparable<PathEntry> {
289
290 final Path abs;
291 final Path path;
292 final Map<String, Object> attributes;
293
294 public PathEntry(Path abs, Path root) {
295 this.abs = abs;
296 this.path = abs.startsWith(root) ? root.relativize(abs) : abs;
297 this.attributes = readAttributes(abs);
298 }
299
300 @Override
301 public int compareTo(PathEntry o) {
302 return path.toString().compareTo(o.path.toString());
303 }
304
305 boolean isNotDirectory() {
306 return is("isRegularFile") || is("isSymbolicLink") || is("isOther");
307 }
308
309 boolean isDirectory() {
310 return is("isDirectory");
311 }
312
313 private boolean is(String attr) {
314 Object d = attributes.get(attr);
315 return d instanceof Boolean && (Boolean) d;
316 }
317
318 String display() {
319 String suffix;
320 String link = "";
321 if (is("isSymbolicLink")) {
322 suffix = "@";
323 try {
324 Path l = Files.readSymbolicLink(abs);
325 link = " -> " + l.toString();
326 } catch (IOException e) {
327
328 }
329 } else if (is("isDirectory")) {
330 suffix = "/";
331 } else if (is("isExecutable")) {
332 suffix = "*";
333 } else if (is("isOther")) {
334 suffix = "";
335 } else {
336 suffix = "";
337 }
338 return path.toString() + suffix + link;
339 }
340
341 String longDisplay() {
342 String username;
343 if (attributes.containsKey("owner")) {
344 username = Objects.toString(attributes.get("owner"), null);
345 } else {
346 username = "owner";
347 }
348 if (username.length() > 8) {
349 username = username.substring(0, 8);
350 } else {
351 for (int i = username.length(); i < 8; i++) {
352 username = username + " ";
353 }
354 }
355 String group;
356 if (attributes.containsKey("group")) {
357 group = Objects.toString(attributes.get("group"), null);
358 } else {
359 group = "group";
360 }
361 if (group.length() > 8) {
362 group = group.substring(0, 8);
363 } else {
364 for (int i = group.length(); i < 8; i++) {
365 group = group + " ";
366 }
367 }
368 Number length = (Number) attributes.get("size");
369 if (length == null) {
370 length = 0L;
371 }
372 String lengthString;
373 if (true ) {
374 double l = length.longValue();
375 String unit = "B";
376 if (l >= 1000) {
377 l /= 1024;
378 unit = "K";
379 if (l >= 1000) {
380 l /= 1024;
381 unit = "M";
382 if (l >= 1000) {
383 l /= 1024;
384 unit = "T";
385 }
386 }
387 }
388 if (l < 10 && length.longValue() > 1000) {
389 lengthString = String.format("%.1f", l) + unit;
390 } else {
391 lengthString = String.format("%3.0f", l) + unit;
392 }
393 } else {
394 lengthString = String.format("%1$8s", length);
395 }
396 @SuppressWarnings("unchecked")
397 Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
398 if (perms == null) {
399 perms = EnumSet.noneOf(PosixFilePermission.class);
400 }
401
402 return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" : (is("isOther") ? "o" : "-")))
403 + PosixFilePermissions.toString(perms) + " "
404 + String.format(
405 "%3s",
406 (attributes.containsKey("nlink")
407 ? attributes.get("nlink").toString()
408 : "1"))
409 + " " + username + " " + group + " " + lengthString + " "
410 + toString((FileTime) attributes.get("lastModifiedTime"))
411 + " " + display();
412 }
413
414 protected String toString(FileTime time) {
415 long millis = (time != null) ? time.toMillis() : -1L;
416 if (millis < 0L) {
417 return "------------";
418 }
419 ZonedDateTime dt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
420
421 if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 60L * 1000L) {
422 return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
423 }
424
425 else {
426 return DateTimeFormatter.ofPattern("MMM ppd yyyy").format(dt);
427 }
428 }
429
430 protected Map<String, Object> readAttributes(Path path) {
431 Map<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
432 for (String view : path.getFileSystem().supportedFileAttributeViews()) {
433 try {
434 Map<String, Object> ta = Files.readAttributes(path, view + ":*", LinkOption.NOFOLLOW_LINKS);
435 ta.forEach(attrs::putIfAbsent);
436 } catch (IOException e) {
437
438 }
439 }
440 attrs.computeIfAbsent("isExecutable", s -> Files.isExecutable(path));
441 attrs.computeIfAbsent("permissions", s -> getPermissionsFromFile(path.toFile()));
442 return attrs;
443 }
444 }
445
446 Files.walk(currentDir)
447 .map(p -> new PathEntry(p, currentDir))
448 .sorted()
449 .map(PathEntry::longDisplay)
450 .forEach(out);
451 }
452
453 private static Set<PosixFilePermission> getPermissionsFromFile(File f) {
454 Set<PosixFilePermission> perms = EnumSet.noneOf(PosixFilePermission.class);
455 if (f.canRead()) {
456 perms.add(PosixFilePermission.OWNER_READ);
457 perms.add(PosixFilePermission.GROUP_READ);
458 perms.add(PosixFilePermission.OTHERS_READ);
459 }
460
461 if (f.canWrite()) {
462 perms.add(PosixFilePermission.OWNER_WRITE);
463 perms.add(PosixFilePermission.GROUP_WRITE);
464 perms.add(PosixFilePermission.OTHERS_WRITE);
465 }
466
467 if (f.canExecute() ) {
468 perms.add(PosixFilePermission.OWNER_EXECUTE);
469 perms.add(PosixFilePermission.GROUP_EXECUTE);
470 perms.add(PosixFilePermission.OTHERS_EXECUTE);
471 }
472
473 return perms;
474 }
475 }