View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.io.file;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  
24  import java.io.IOException;
25  import java.net.URI;
26  import java.net.URL;
27  import java.nio.file.CopyOption;
28  import java.nio.file.FileSystem;
29  import java.nio.file.FileSystems;
30  import java.nio.file.Files;
31  import java.nio.file.LinkOption;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.HashMap;
35  import java.util.Map;
36  import java.util.stream.Stream;
37  
38  import org.apache.commons.io.FilenameUtils;
39  import org.apache.commons.io.file.Counters.PathCounters;
40  import org.junit.jupiter.api.Test;
41  import org.junit.jupiter.api.extension.ExtensionContext;
42  import org.junit.jupiter.api.io.TempDir;
43  import org.junit.jupiter.params.ParameterizedTest;
44  import org.junit.jupiter.params.provider.Arguments;
45  import org.junit.jupiter.params.provider.ArgumentsProvider;
46  import org.junit.jupiter.params.provider.ArgumentsSource;
47  import org.junit.jupiter.params.support.ParameterDeclarations;
48  
49  /**
50   * Tests {@link PathUtils}.
51   */
52  class PathUtilsCopyTest extends AbstractTempDirTest {
53  
54      private static class CopyOptionsArgumentsProvider implements ArgumentsProvider {
55  
56          @Override
57          public Stream<? extends Arguments> provideArguments(final ParameterDeclarations parameters, final ExtensionContext context) {
58              return Stream.of(
59                      Arguments.of((Object) new CopyOption[0]),
60                      Arguments.of((Object) new CopyOption[] { LinkOption.NOFOLLOW_LINKS })
61              );
62          }
63      }
64  
65      private static final String TEST_JAR_NAME = "test.jar";
66  
67      private static final String TEST_JAR_PATH = "src/test/resources/org/apache/commons/io/test.jar";
68  
69      private FileSystem openArchive(final Path p, final boolean createNew) throws IOException {
70          if (createNew) {
71              final Map<String, String> env = new HashMap<>();
72              env.put("create", "true");
73              final URI fileUri = p.toAbsolutePath().toUri();
74              final URI uri = URI.create("jar:" + fileUri.toASCIIString());
75              return FileSystems.newFileSystem(uri, env, null);
76          }
77          return FileSystems.newFileSystem(p, (ClassLoader) null);
78      }
79  
80      @Test
81      void testCopyDirectoryCyclicSymbolicLink() throws Exception {
82          // sourceDir = tempDirPath/source
83          final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
84          // dir1 = tempDirPath/source/dir1
85          final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
86          // dir2 = tempDirPath/source/dir1/dir2
87          final Path dir2 = Files.createDirectory(dir1.resolve("dir2"));
88          // link = tempDirPath/source/dir1/dir2/cyclic-symlink
89          // target = ..
90          Files.createSymbolicLink(dir2.resolve("cyclic-symlink"), dir2.relativize(dir1));
91          final Path targetDir = tempDirPath.resolve("target");
92          PathUtils.copyDirectory(sourceDir, targetDir);
93          assertTrue(Files.exists(targetDir));
94          final Path copyOfDir2 = targetDir.resolve("dir1").resolve("dir2");
95          assertTrue(Files.exists(copyOfDir2));
96          assertTrue(Files.isDirectory(copyOfDir2));
97          assertTrue(Files.exists(copyOfDir2.resolve("cyclic-symlink")));
98      }
99  
100     @Test
101     void testCopyDirectoryFollowsAbsoluteSymbolicLinkToDirectory() throws Exception {
102         // Given
103         final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
104         final Path dir1 = Files.createDirectory(externalDir.resolve("dir1"));
105         final Path file2 = Files.write(dir1.resolve("file2"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
106         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
107         final Path dir3 = Files.createDirectory(sourceDir.resolve("dir3"));
108         final Path file4 = Files.write(dir3.resolve("file4"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
109         Files.createSymbolicLink(sourceDir.resolve("symlink1"), dir1.toAbsolutePath());
110         Files.createSymbolicLink(sourceDir.resolve("symlink2"), sourceDir.relativize(file2));
111         Files.createSymbolicLink(sourceDir.resolve("symlink3"), sourceDir.relativize(dir3));
112         Files.createSymbolicLink(dir3.resolve("symlink4"), file4.toAbsolutePath());
113         final Path targetDir = tempDirPath.resolve("target");
114         // When
115         final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir);
116         // Then
117         // 6 * 11 bytes == 66:
118         // file2
119         // file4
120         // symlink2 -> file2
121         // symlink4 -> file4
122         // symlink1 -> dir1 containing file2
123         // symlink3 -> dir3 containing file4
124         //
125         // Different result value depending on the Java version and operating system, but should not throw an exception or loop infinitely.
126         pathCounters.getByteCounter().get();
127         assertEquals(2L, pathCounters.getDirectoryCounter().get());
128         assertEquals(5L, pathCounters.getFileCounter().get());
129         assertTrue(Files.exists(targetDir.resolve("dir3").resolve("file4")));
130         assertTrue(Files.exists(targetDir.resolve("dir3").resolve("symlink4")));
131     }
132 
133     @Test
134     void testCopyDirectoryForDifferentFilesystemsWithAbsolutePath() throws IOException {
135         final Path archivePath = Paths.get(TEST_JAR_PATH);
136         try (FileSystem archive = openArchive(archivePath, false)) {
137             // relative jar -> absolute dir
138             Path sourceDir = archive.getPath("dir1");
139             PathUtils.copyDirectory(sourceDir, tempDirPath);
140             assertTrue(Files.exists(tempDirPath.resolve("f1")));
141             // absolute jar -> absolute dir
142             sourceDir = archive.getPath("/next");
143             PathUtils.copyDirectory(sourceDir, tempDirPath);
144             assertTrue(Files.exists(tempDirPath.resolve("dir")));
145         }
146     }
147 
148     @Test
149     void testCopyDirectoryForDifferentFilesystemsWithAbsolutePathReverse() throws IOException {
150         try (FileSystem archive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) {
151             // absolute dir -> relative jar
152             Path targetDir = archive.getPath("target");
153             Files.createDirectory(targetDir);
154             final Path sourceDir = Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-2").toAbsolutePath();
155             PathUtils.copyDirectory(sourceDir, targetDir);
156             assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1")));
157             // absolute dir -> absolute jar
158             targetDir = archive.getPath("/");
159             PathUtils.copyDirectory(sourceDir, targetDir);
160             assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1")));
161         }
162     }
163 
164     @Test
165     void testCopyDirectoryForDifferentFilesystemsWithRelativePath() throws IOException {
166         final Path archivePath = Paths.get(TEST_JAR_PATH);
167         try (FileSystem archive = openArchive(archivePath, false); FileSystem targetArchive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) {
168             final Path targetDir = targetArchive.getPath("targetDir");
169             Files.createDirectory(targetDir);
170             // relative jar -> relative dir
171             Path sourceDir = archive.getPath("next");
172             PathUtils.copyDirectory(sourceDir, targetDir);
173             assertTrue(Files.exists(targetDir.resolve("dir")));
174             // absolute jar -> relative dir
175             sourceDir = archive.getPath("/dir1");
176             PathUtils.copyDirectory(sourceDir, targetDir);
177             assertTrue(Files.exists(targetDir.resolve("f1")));
178         }
179     }
180 
181     @Test
182     void testCopyDirectoryForDifferentFilesystemsWithRelativePathReverse() throws IOException {
183         try (FileSystem archive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) {
184             // relative dir -> relative jar
185             Path targetDir = archive.getPath("target");
186             Files.createDirectory(targetDir);
187             final Path sourceDir = Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-2");
188             PathUtils.copyDirectory(sourceDir, targetDir);
189             assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1")));
190             // relative dir -> absolute jar
191             targetDir = archive.getPath("/");
192             PathUtils.copyDirectory(sourceDir, targetDir);
193             assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1")));
194         }
195     }
196 
197     @ParameterizedTest
198     @ArgumentsSource(CopyOptionsArgumentsProvider.class)
199     void testCopyDirectoryIgnoresBrokenSymbolicLink(final CopyOption... copyOptions) throws Exception {
200         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
201         final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
202         Files.createSymbolicLink(dir.resolve("broken-symlink"), dir.relativize(sourceDir.resolve("file")));
203         final Path targetDir = tempDirPath.resolve("target");
204         PathUtils.copyDirectory(sourceDir, targetDir, copyOptions);
205         assertTrue(Files.exists(targetDir));
206         final Path copyOfDir = targetDir.resolve("dir");
207         assertTrue(Files.exists(copyOfDir));
208         assertTrue(Files.isDirectory(copyOfDir));
209         assertFalse(Files.exists(copyOfDir.resolve("broken-symlink")));
210     }
211 
212     /**
213      * Source tree:
214      * <pre>
215      *
216      * source/
217      *   dir/
218      *     file
219      *     symlink-to-file
220      *   symlink-to-dir
221      * </pre>
222      */
223     @Test
224     void testCopyDirectoryPreservesSymlinks(@TempDir final Path tempDir) throws Exception {
225         final Path sourceDir = Files.createDirectory(tempDir.resolve("source"));
226         final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
227         final Path dirLink = Files.createSymbolicLink(sourceDir.resolve("link-to-dir"), dir);
228         assertTrue(Files.exists(dirLink));
229         final Path file = Files.createFile(dir.resolve("file"));
230         final Path fileLink = Files.createSymbolicLink(dir.resolve("link-to-file"), file);
231         assertTrue(Files.exists(fileLink));
232         final Path targetDir = tempDir.resolve("target");
233         PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
234         final Path copyOfDir = targetDir.resolve("dir");
235         assertTrue(Files.exists(copyOfDir));
236         final Path copyOfDirLink = targetDir.resolve("link-to-dir");
237         assertTrue(Files.exists(copyOfDirLink));
238         assertTrue(Files.isSymbolicLink(copyOfDirLink));
239         final Path copyOfFileLink = copyOfDir.resolve("link-to-file");
240         assertTrue(Files.exists(copyOfFileLink));
241         assertTrue(Files.isSymbolicLink(copyOfFileLink));
242     }
243 
244     /**
245      * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves absolute symlinks to directories.
246      * This simulates to the behavior of Linux {@code cp -r}.
247      * Given the source directory structure:
248      * <pre>{@code
249      * user@host:/tmp$ tree source/ external/
250      * source/
251      * └── dir
252      *     └── symlink -> /tmp/external
253      * external/
254      * }</pre>
255      * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
256      * <pre>{@code
257      * user@host:/tmp$ tree target/
258      * target/
259      * └── dir
260      *     └── symlink -> /tmp/external
261      * }</pre>
262      */
263     @Test
264     void testCopyDirectoryWithNoFollowLinksPreservesAbsoluteSymbolicLinkToDir() throws Exception {
265         // Given
266         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
267         final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
268         final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
269         // source/dir/symlink -> /tmp/external
270         Files.createSymbolicLink(dir.resolve("symlink"), externalDir.toAbsolutePath());
271         final Path targetDir = tempDirPath.resolve("target");
272         // When
273         final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
274         // Then
275         // assertEquals(0L, pathCounters.getByteCounter().get());
276         assertEquals(2L, pathCounters.getDirectoryCounter().get());
277         // Verify that symlink with NOFOLLOW_LINKS counts as file
278         assertEquals(1L, pathCounters.getFileCounter().get());
279         final Path copyOfAbsoluteSymlinkToDir = targetDir.resolve("dir").resolve("symlink");
280         assertTrue(Files.isSymbolicLink(copyOfAbsoluteSymlinkToDir));
281         assertTrue(Files.isDirectory(copyOfAbsoluteSymlinkToDir));
282         // Verify that target/dir/symlink resolves to /tmp/external
283         assertEquals(externalDir.toRealPath(), copyOfAbsoluteSymlinkToDir.toRealPath());
284     }
285 
286     /**
287      * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to files.
288      * This simulates to the behavior of Linux {@code cp -r}.
289      * Given the source directory structure:
290      * <pre>{@code
291      * user@host:/tmp$ tree source/
292      * source/
293      * ├── dir
294      * │   └── symlink -> ../file
295      * └── file
296      * }</pre>
297      * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
298      * <pre>{@code
299      * user@host:/tmp$ tree target/
300      * target/
301      * ├── dir
302      * │   └── symlink -> ../file
303      * └── file
304      * }</pre>
305      */
306     @Test
307     void testCopyDirectoryWithNoFollowLinksPreservesAbsoluteSymbolicLinkToFile() throws Exception {
308         // Given
309         final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
310         final Path file = Files.write(externalDir.resolve("file"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
311         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
312         final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
313         // source/dir/symlink -> /tmp/file
314         Files.createSymbolicLink(dir.resolve("symlink"), file.toAbsolutePath());
315         final Path targetDir = tempDirPath.resolve("target");
316         // When
317         final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
318         // Then
319         // assertEquals(0L, pathCounters.getByteCounter().get());
320         assertEquals(2L, pathCounters.getDirectoryCounter().get());
321         assertEquals(1L, pathCounters.getFileCounter().get());
322         final Path copyOfAbsoluteSymlinkToFile = targetDir.resolve("dir").resolve("symlink");
323         assertTrue(Files.isSymbolicLink(copyOfAbsoluteSymlinkToFile));
324         assertTrue(Files.isRegularFile(copyOfAbsoluteSymlinkToFile));
325         // Verify that /tmp/target/dir/symlink resolves to /tmp/source/file
326         assertEquals(file.toRealPath(), copyOfAbsoluteSymlinkToFile.toRealPath());
327     }
328 
329     @Test
330     void testCopyDirectoryWithNoFollowLinksPreservesCyclicSymbolicLink() throws Exception {
331         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
332         final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
333         final Path dir2 = Files.createDirectory(dir1.resolve("dir2"));
334         Files.createSymbolicLink(dir2.resolve("cyclic-symlink"), dir2.relativize(dir1));
335         final Path targetDir = tempDirPath.resolve("target");
336         PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
337         assertTrue(Files.exists(targetDir));
338         final Path copyOfDir1 = targetDir.resolve("dir1");
339         final Path copyOfDir2 = copyOfDir1.resolve("dir2");
340         assertTrue(Files.exists(copyOfDir2));
341         assertTrue(Files.isDirectory(copyOfDir2));
342         final Path copyOfCyclicSymlink = copyOfDir2.resolve("cyclic-symlink");
343         assertTrue(Files.exists(copyOfCyclicSymlink));
344         assertEquals(copyOfDir1.toRealPath(), copyOfCyclicSymlink.toRealPath());
345     }
346 
347     /**
348      * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to directories.
349      * This simulates to the behavior of Linux {@code cp -r}.
350      * Given the source directory structure:
351      * <pre>{@code
352      * user@host:/tmp$ tree source/
353      * source/
354      * ├── dir1
355      * │   └── symlink -> ../dir2
356      * └── dir2
357      * }</pre>
358      * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
359      * <pre>{@code
360      * user@host:/tmp$ tree target/
361      * target/
362      * ├── dir1
363      * │   └── symlink -> ../dir2
364      * └── dir2
365      * }</pre>
366      */
367     @Test
368     void testCopyDirectoryWithNoFollowLinksPreservesRelativeSymbolicLinkToDir() throws Exception {
369         // Given
370         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
371         final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
372         final Path dir2 = Files.createDirectory(sourceDir.resolve("dir2"));
373         // source/dir1/symlink -> ../dir2
374         Files.createSymbolicLink(dir1.resolve("symlink"), dir1.relativize(dir2));
375         final Path targetDir = tempDirPath.resolve("target");
376         // When
377         final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
378         // Then
379         // assertEquals(0L, pathCounters.getByteCounter().get());
380         assertEquals(3L, pathCounters.getDirectoryCounter().get());
381         // Verify that symlink with NOFOLLOW_LINKS counts as file
382         assertEquals(1L, pathCounters.getFileCounter().get());
383         final Path copyOfDir2 = targetDir.resolve("dir2");
384         final Path copyOfRelativeSymlinkToDir2 = targetDir.resolve("dir1").resolve("symlink");
385         assertTrue(Files.isSymbolicLink(copyOfRelativeSymlinkToDir2));
386         assertTrue(Files.isDirectory(copyOfRelativeSymlinkToDir2));
387         // Verify that target/dir1/symlink resolves to /tmp/target/dir2
388         assertEquals(copyOfDir2.toRealPath(), copyOfRelativeSymlinkToDir2.toRealPath());
389     }
390 
391     /**
392      * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to files.
393      * This simulates to the behavior of Linux {@code cp -r}.
394      * Given the source directory structure:
395      * <pre>{@code
396      * user@host:/tmp$ tree source/
397      * source/
398      * ├── dir
399      * │   └── symlink -> ../file
400      * └── file
401      * }</pre>
402      * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
403      * <pre>{@code
404      * user@host:/tmp$ tree target/
405      * target/
406      * ├── dir
407      * │   └── symlink -> ../file
408      * └── file
409      * }</pre>
410      */
411     @Test
412     void testCopyDirectoryWithNoFollowLinksPreservesRelativeSymbolicLinkToFile() throws Exception {
413         // Given
414         final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
415         final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
416         final Path file = Files.write(sourceDir.resolve("file"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
417         // source/dir/symlink -> ../file
418         Files.createSymbolicLink(dir.resolve("symlink"), dir.relativize(file));
419         final Path targetDir = tempDirPath.resolve("target");
420         // When
421         final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
422         // Then
423         // assertEquals(11L, pathCounters.getByteCounter().get());
424         assertEquals(2L, pathCounters.getDirectoryCounter().get());
425         // Verify that file + symlink with NOFOLLOW_LINKS counts as 2 files
426         assertEquals(2L, pathCounters.getFileCounter().get());
427         final Path copyOfFile = targetDir.resolve("file");
428         final Path copyOfRelativeSymlinkToFile = targetDir.resolve("dir").resolve("symlink");
429         assertTrue(Files.isSymbolicLink(copyOfRelativeSymlinkToFile));
430         assertTrue(Files.isRegularFile(copyOfRelativeSymlinkToFile));
431         // Verify that /tmp/target/dir/symlink resolves to /tmp/target/file
432         assertEquals(copyOfFile.toRealPath(), copyOfRelativeSymlinkToFile.toRealPath());
433     }
434 
435     @Test
436     void testCopyFile() throws IOException {
437         final Path sourceFile = Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/file-size-1.bin");
438         final Path targetFile = PathUtils.copyFileToDirectory(sourceFile, tempDirPath);
439         assertTrue(Files.exists(targetFile));
440         assertEquals(Files.size(sourceFile), Files.size(targetFile));
441     }
442 
443     @Test
444     void testCopyFileTwoFileSystem() throws IOException {
445         try (FileSystem archive = openArchive(Paths.get(TEST_JAR_PATH), false)) {
446             final Path sourceFile = archive.getPath("next/dir/test.log");
447             final Path targetFile = PathUtils.copyFileToDirectory(sourceFile, tempDirPath);
448             assertTrue(Files.exists(targetFile));
449             assertEquals(Files.size(sourceFile), Files.size(targetFile));
450         }
451     }
452 
453     @Test
454     void testCopyURL() throws IOException {
455         final Path sourceFile = Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/file-size-1.bin");
456         final URL url = new URL("file:///" + FilenameUtils.getPath(sourceFile.toAbsolutePath().toString()) + sourceFile.getFileName());
457         final Path targetFile = PathUtils.copyFileToDirectory(url, tempDirPath);
458         assertTrue(Files.exists(targetFile));
459         assertEquals(Files.size(sourceFile), Files.size(targetFile));
460     }
461 }