This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
commit 2adf0f089dbb0528056ffc6d9570756e0bbe2bce Author: Gary Gregory <gardgreg...@gmail.com> AuthorDate: Sun Feb 6 11:42:12 2022 -0500 Add PathUtils.createParentDirectories(Path, LinkOption, FileAttribute<?>...) --- src/changes/changes.xml | 3 + .../java/org/apache/commons/io/file/PathUtils.java | 159 +++++++++++++-------- .../org/apache/commons/io/file/PathUtilsTest.java | 17 ++- 3 files changed, 117 insertions(+), 62 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 74060bd..769d199 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -320,6 +320,9 @@ The <action> type attribute can be add,update,fix,remove. <action dev="ggregory" type="add" due-to="Gary Gregory"> Add CharsetDecoders. </action> + <action dev="ggregory" type="add" due-to="Gary Gregory"> + Add PathUtils.createParentDirectories(Path, LinkOption, FileAttribute...). + </action> <!-- UPDATE --> <action dev="ggregory" type="add" due-to="Gary Gregory"> Update FileEntry to use FileTime instead of long for file time stamps. diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java b/src/main/java/org/apache/commons/io/file/PathUtils.java index a15b944..1795eed 100644 --- a/src/main/java/org/apache/commons/io/file/PathUtils.java +++ b/src/main/java/org/apache/commons/io/file/PathUtils.java @@ -81,7 +81,8 @@ import org.apache.commons.io.function.IOFunction; public final class PathUtils { /** - * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted relative lists when comparing directories. + * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted relative + * lists when comparing directories. */ private static class RelativeSortedPaths { @@ -102,7 +103,7 @@ public final class PathUtils { * @throws IOException if an I/O error is thrown by a visitor method. */ private RelativeSortedPaths(final Path dir1, final Path dir2, final int maxDepth, final LinkOption[] linkOptions, - final FileVisitOption[] fileVisitOptions) throws IOException { + final FileVisitOption[] fileVisitOptions) throws IOException { final List<Path> tmpRelativeDirList1; final List<Path> tmpRelativeDirList2; List<Path> tmpRelativeFileList1 = null; @@ -141,9 +142,9 @@ public final class PathUtils { } } - private static final OpenOption[] OPEN_OPTIONS_TRUNCATE = { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING }; + private static final OpenOption[] OPEN_OPTIONS_TRUNCATE = {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING}; - private static final OpenOption[] OPEN_OPTIONS_APPEND = { StandardOpenOption.CREATE, StandardOpenOption.APPEND }; + private static final OpenOption[] OPEN_OPTIONS_APPEND = {StandardOpenOption.CREATE, StandardOpenOption.APPEND}; /** * Empty {@link CopyOption} array. @@ -174,7 +175,14 @@ public final class PathUtils { * * @since 2.9.0 */ - public static final LinkOption[] NOFOLLOW_LINK_OPTION_ARRAY = { LinkOption.NOFOLLOW_LINKS }; + public static final LinkOption[] NOFOLLOW_LINK_OPTION_ARRAY = {LinkOption.NOFOLLOW_LINKS}; + + /** + * A LinkOption used to follow link in this class, the inverse of {@link LinkOption#NOFOLLOW_LINKS}. + * + * @since 2.12.0 + */ + public static final LinkOption FOLLOW_LINKS = null; /** * Empty {@link OpenOption} array. @@ -251,7 +259,7 @@ public final class PathUtils { public static PathCounters copyDirectory(final Path sourceDirectory, final Path targetDirectory, final CopyOption... copyOptions) throws IOException { final Path absoluteSource = sourceDirectory.toAbsolutePath(); return visitFileTree(new CopyDirectoryVisitor(Counters.longPathCounters(), absoluteSource, targetDirectory, copyOptions), absoluteSource) - .getPathCounters(); + .getPathCounters(); } /** @@ -335,10 +343,29 @@ public final class PathUtils { * @since 2.9.0 */ public static Path createParentDirectories(final Path path, final FileAttribute<?>... attrs) throws IOException { - final Path parent = getParent(path); + return createParentDirectories(path, LinkOption.NOFOLLOW_LINKS, attrs); + } + + /** + * Creates the parent directories for the given {@code path}. + * + * @param path The path to a file (or directory). + * @param linkOption A {@link LinkOption} or null. + * @param attrs An optional list of file attributes to set atomically when creating the directories. + * @return The Path for the {@code path}'s parent directory or null if the given path has no parent. + * @throws IOException if an I/O error occurs. + * @since 2.12.0 + */ + public static Path createParentDirectories(final Path path, final LinkOption linkOption, final FileAttribute<?>... attrs) throws IOException { + Path parent = getParent(path); + parent = linkOption == LinkOption.NOFOLLOW_LINKS ? parent : readIfSymbolicLink(parent); return parent == null ? null : Files.createDirectories(parent, attrs); } + private static Path readIfSymbolicLink(Path path) throws IOException { + return Files.isSymbolicLink(path) ? Files.readSymbolicLink(path) : path; + } + /** * Gets the current directory. * @@ -438,7 +465,7 @@ public final class PathUtils { final LinkOption[] linkOptions = PathUtils.NOFOLLOW_LINK_OPTION_ARRAY; // POSIX ops will noop on non-POSIX. return withPosixFileAttributes(getParent(directory), linkOptions, overrideReadOnly(deleteOptions), - pfa -> visitFileTree(new DeletingPathVisitor(Counters.longPathCounters(), linkOptions, deleteOptions), directory).getPathCounters()); + pfa -> visitFileTree(new DeletingPathVisitor(Counters.longPathCounters(), linkOptions, deleteOptions), directory).getPathCounters()); } /** @@ -536,8 +563,8 @@ public final class PathUtils { } /** - * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all - * sub-directories. + * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The + * comparison includes all files in all sub-directories. * * @param path1 The first directory. * @param path2 The second directory. @@ -549,8 +576,8 @@ public final class PathUtils { } /** - * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all - * sub-directories. + * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The + * comparison includes all files in all sub-directories. * * @param path1 The first directory. * @param path2 The second directory. @@ -561,7 +588,7 @@ public final class PathUtils { * @throws IOException if an I/O error is thrown by a visitor method. */ public static boolean directoryAndFileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions, final OpenOption[] openOptions, - final FileVisitOption[] fileVisitOption) throws IOException { + final FileVisitOption[] fileVisitOption) throws IOException { // First walk both file trees and gather normalized paths. if (path1 == null && path2 == null) { return true; @@ -593,8 +620,8 @@ public final class PathUtils { } /** - * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The comparison includes all files in all - * sub-directories. + * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The + * comparison includes all files in all sub-directories. * * @param path1 The first directory. * @param path2 The second directory. @@ -606,8 +633,8 @@ public final class PathUtils { } /** - * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The comparison includes all files in all - * sub-directories. + * Compares the file sets of two Paths to determine if they are equal or not without considering file contents. The + * comparison includes all files in all sub-directories. * * @param path1 The first directory. * @param path2 The second directory. @@ -618,7 +645,7 @@ public final class PathUtils { * @throws IOException if an I/O error is thrown by a visitor method. */ public static boolean directoryContentEquals(final Path path1, final Path path2, final int maxDepth, final LinkOption[] linkOptions, - final FileVisitOption[] fileVisitOptions) throws IOException { + final FileVisitOption[] fileVisitOptions) throws IOException { return new RelativeSortedPaths(path1, path2, maxDepth, linkOptions, fileVisitOptions).equals; } @@ -659,7 +686,7 @@ public final class PathUtils { * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File) */ public static boolean fileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions, final OpenOption[] openOptions) - throws IOException { + throws IOException { if (path1 == null && path2 == null) { return true; } @@ -694,15 +721,15 @@ public final class PathUtils { return true; } try (final InputStream inputStream1 = Files.newInputStream(nPath1, openOptions); - final InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) { + final InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) { return IOUtils.contentEquals(inputStream1, inputStream2); } } /** * <p> - * Applies an {@link IOFileFilter} to the provided {@link File} objects. The resulting array is a subset of the original file list that matches the provided - * filter. + * Applies an {@link IOFileFilter} to the provided {@link File} objects. The resulting array is a subset of the original + * file list that matches the provided filter. * </p> * * <p> @@ -819,10 +846,10 @@ public final class PathUtils { * * @param path the path to the file. * @param options options indicating how to handle symbolic links - * @return {@code true} if the file is a directory; {@code false} if the path is null, the file does not exist, is not a directory, or it cannot be - * determined if the file is a directory or not. - * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String) - * checkRead} method is invoked to check read access to the directory. + * @return {@code true} if the file is a directory; {@code false} if the path is null, the file does not exist, is not a + * directory, or it cannot be determined if the file is a directory or not. + * @throws SecurityException In the case of the default provider, and a security manager is installed, the + * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory. * @since 2.9.0 */ public static boolean isDirectory(final Path path, final LinkOption... options) { @@ -845,10 +872,11 @@ public final class PathUtils { * * @param directory the directory to query. * @return whether the directory is empty. - * @throws NotDirectoryException if the file could not otherwise be opened because it is not a directory <i>(optional specific exception)</i>. + * @throws NotDirectoryException if the file could not otherwise be opened because it is not a directory <i>(optional + * specific exception)</i>. * @throws IOException if an I/O error occurs. - * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String) - * checkRead} method is invoked to check read access to the directory. + * @throws SecurityException In the case of the default provider, and a security manager is installed, the + * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory. */ public static boolean isEmptyDirectory(final Path directory) throws IOException { try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) { @@ -862,8 +890,8 @@ public final class PathUtils { * @param file the file to query. * @return whether the file is empty. * @throws IOException if an I/O error occurs. - * @throws SecurityException In the case of the default provider, and a security manager is installed, its {@link SecurityManager#checkRead(String) - * checkRead} method denies read access to the file. + * @throws SecurityException In the case of the default provider, and a security manager is installed, its + * {@link SecurityManager#checkRead(String) checkRead} method denies read access to the file. */ public static boolean isEmptyFile(final Path file) throws IOException { return Files.size(file) <= 0; @@ -1025,10 +1053,10 @@ public final class PathUtils { * * @param path the path to the file. * @param options options indicating how to handle symbolic links. - * @return {@code true} if the file is a regular file; {@code false} if the path is null, the file does not exist, is not a directory, or it cannot be - * determined if the file is a regular file or not. - * @throws SecurityException In the case of the default provider, and a security manager is installed, the {@link SecurityManager#checkRead(String) - * checkRead} method is invoked to check read access to the directory. + * @return {@code true} if the file is a regular file; {@code false} if the path is null, the file does not exist, is + * not a directory, or it cannot be determined if the file is a regular file or not. + * @throws SecurityException In the case of the default provider, and a security manager is installed, the + * {@link SecurityManager#checkRead(String) checkRead} method is invoked to check read access to the directory. * @since 2.9.0 */ public static boolean isRegularFile(final Path path, final LinkOption... options) { @@ -1048,7 +1076,8 @@ public final class PathUtils { } /** - * Creates a new OutputStream by opening or creating a file, returning an output stream that may be used to write bytes to the file. + * Creates a new OutputStream by opening or creating a file, returning an output stream that may be used to write bytes + * to the file. * * @param path the Path. * @param append Whether or not to append. @@ -1086,8 +1115,8 @@ public final class PathUtils { } /** - * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. Throws - * {@link UncheckedIOExceptions} instead of {@link IOException}. + * Reads the BasicFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. Throws {@link UncheckedIOExceptions} instead of {@link IOException}. * * @param <A> The {@code BasicFileAttributes} type * @param path The Path to test. @@ -1123,7 +1152,8 @@ public final class PathUtils { } /** - * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. + * Reads the BasicFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. * * @param path the path to read. * @param options options indicating how to handle symbolic links. @@ -1135,7 +1165,8 @@ public final class PathUtils { } /** - * Reads the BasicFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. + * Reads the BasicFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. * * @param path the path to read. * @return the path attributes. @@ -1149,7 +1180,8 @@ public final class PathUtils { } /** - * Reads the DosFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. + * Reads the DosFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. * * @param path the path to read. * @param options options indicating how to handle symbolic links. @@ -1161,7 +1193,8 @@ public final class PathUtils { } /** - * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. + * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. * * @param path The Path to read. * @param options options indicating how to handle symbolic links. @@ -1174,7 +1207,8 @@ public final class PathUtils { } /** - * Reads the PosixFileAttributes from the given path. Returns null instead of throwing {@link UnsupportedOperationException}. + * Reads the PosixFileAttributes from the given path. Returns null instead of throwing + * {@link UnsupportedOperationException}. * * @param path The Path to read. * @param options options indicating how to handle symbolic links. @@ -1217,7 +1251,8 @@ public final class PathUtils { } /** - * Throws an {@link IllegalArgumentException} if the file is not writable. This provides a more precise exception message than a plain access denied. + * Throws an {@link IllegalArgumentException} if the file is not writable. This provides a more precise exception + * message than a plain access denied. * * @param file The file to test. * @param name The parameter name to use in the exception message. @@ -1300,7 +1335,7 @@ public final class PathUtils { * @throws IOException if an I/O error occurs. */ private static boolean setPosixDeletePermissions(final Path parent, final boolean enableDeleteChildren, final LinkOption... linkOptions) - throws IOException { + throws IOException { // To delete a file in POSIX, you need write and execute permissions on its parent directory. // @formatter:off return setPosixPermissions(parent, enableDeleteChildren, Arrays.asList( @@ -1325,7 +1360,7 @@ public final class PathUtils { * @throws IOException if an I/O error occurs. */ private static boolean setPosixPermissions(final Path path, final boolean addPermissions, final List<PosixFilePermission> updatePermissions, - final LinkOption... linkOptions) throws IOException { + final LinkOption... linkOptions) throws IOException { if (path != null) { final Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path, linkOptions); if (addPermissions) { @@ -1407,11 +1442,11 @@ public final class PathUtils { } /** - * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size is returned. If the argument is a - * directory, then the size of the directory is calculated recursively. + * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size + * is returned. If the argument is a directory, then the size of the directory is calculated recursively. * <p> - * Note that overflow is not detected, and the return value may be negative if overflow occurs. See {@link #sizeOfAsBigInteger(Path)} for an alternative - * method that does not overflow. + * Note that overflow is not detected, and the return value may be negative if overflow occurs. See + * {@link #sizeOfAsBigInteger(Path)} for an alternative method that does not overflow. * </p> * * @param path the regular file or directory to return the size of, must not be {@code null}. @@ -1427,8 +1462,8 @@ public final class PathUtils { } /** - * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size is returned. If the argument is a - * directory, then the size of the directory is calculated recursively. + * Returns the size of the given file or directory. If the provided {@link Path} is a regular file, then the file's size + * is returned. If the argument is a directory, then the size of the directory is calculated recursively. * * @param path the regular file or directory to return the size of (must not be {@code null}). * @return the length of the file, or recursive size of the directory, provided (in bytes). @@ -1445,12 +1480,13 @@ public final class PathUtils { /** * Counts the size of a directory recursively (sum of the size of all files). * <p> - * Note that overflow is not detected, and the return value may be negative if overflow occurs. See {@link #sizeOfDirectoryAsBigInteger(Path)} for an - * alternative method that does not overflow. + * Note that overflow is not detected, and the return value may be negative if overflow occurs. See + * {@link #sizeOfDirectoryAsBigInteger(Path)} for an alternative method that does not overflow. * </p> * * @param directory directory to inspect, must not be {@code null}. - * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total is greater than {@link Long#MAX_VALUE}. + * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total is + * greater than {@link Long#MAX_VALUE}. * @throws NullPointerException if the directory is {@code null}. * @throws IOException if an I/O error occurs. * @since 2.12.0 @@ -1515,7 +1551,7 @@ public final class PathUtils { * @throws IOException if an I/O error is thrown by a visitor method. */ public static <T extends FileVisitor<? super Path>> T visitFileTree(final T visitor, final Path start, final Set<FileVisitOption> options, - final int maxDepth) throws IOException { + final int maxDepth) throws IOException { Files.walkFileTree(start, options, maxDepth, visitor); return visitor; } @@ -1556,7 +1592,8 @@ public final class PathUtils { /** * Waits for the file system to propagate a file creation, with a timeout. * <p> - * This method repeatedly tests {@link Files#exists(Path,LinkOption...)} until it returns true up to the maximum time given. + * This method repeatedly tests {@link Files#exists(Path,LinkOption...)} until it returns true up to the maximum time + * given. * </p> * * @param file the file to check, must not be {@code null}. @@ -1606,13 +1643,13 @@ public final class PathUtils { * @since 2.9.0 */ public static Stream<Path> walk(final Path start, final PathFilter pathFilter, final int maxDepth, final boolean readAttributes, - final FileVisitOption... options) throws IOException { + final FileVisitOption... options) throws IOException { return Files.walk(start, maxDepth, options) - .filter(path -> pathFilter.accept(path, readAttributes ? readBasicFileAttributesUnchecked(path) : null) == FileVisitResult.CONTINUE); + .filter(path -> pathFilter.accept(path, readAttributes ? readBasicFileAttributesUnchecked(path) : null) == FileVisitResult.CONTINUE); } private static <R> R withPosixFileAttributes(final Path path, final LinkOption[] linkOptions, final boolean overrideReadOnly, - final IOFunction<PosixFileAttributes, R> function) throws IOException { + final IOFunction<PosixFileAttributes, R> function) throws IOException { final PosixFileAttributes posixFileAttributes = overrideReadOnly ? readPosixFileAttributes(path, linkOptions) : null; try { return function.apply(posixFileAttributes); @@ -1635,7 +1672,7 @@ public final class PathUtils { * @since 2.12.0 */ public static Path writeString(final Path path, final CharSequence charSequence, final Charset charset, final OpenOption... openOptions) - throws IOException { + throws IOException { // Check the text is not null before opening file. Objects.requireNonNull(path, "path"); Objects.requireNonNull(charSequence, "charSequence"); diff --git a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java index eb4540c..6048b78 100644 --- a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java +++ b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java @@ -180,7 +180,15 @@ public class PathUtilsTest extends AbstractTempDirTest { } @Test - public void testCreateDirectoriesWithClashingSymlink() throws IOException { + public void testCreateDirectoriesSymlink() throws IOException { + final Path symlinkedDir = createTempSymlinkedRelativeDir(); + final String leafDirName = "child"; + final Path newDirFollowed = PathUtils.createParentDirectories(symlinkedDir.resolve(leafDirName), PathUtils.FOLLOW_LINKS); + assertEquals(Files.readSymbolicLink(symlinkedDir), newDirFollowed); + } + + @Test + public void testCreateDirectoriesSymlinkClashing() throws IOException { final Path symlinkedDir = createTempSymlinkedRelativeDir(); assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.createParentDirectories(symlinkedDir.resolve("child"))); } @@ -275,6 +283,13 @@ public class PathUtilsTest extends AbstractTempDirTest { } @Test + public void testNewOutputStreamNewFileInsideExistingSymlinkedDirFollow() throws IOException { + final Path symlinkDir = createTempSymlinkedRelativeDir(); + final Path file = symlinkDir.resolve("test.txt"); + assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.newOutputStream(file, false)); + } + + @Test public void testReadAttributesPosix() throws IOException { boolean isPosix; try {