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
The following commit(s) were added to refs/heads/master by this push: new 580a7b5 [IO-701] Make PathUtils.setReadOnly deal with LinuxDosFileAttributeView. (#186) 580a7b5 is described below commit 580a7b5d395c315e4b7172f5563d3fe7461f92a6 Author: Boris Unckel <bu.github...@mail.unckel.net> AuthorDate: Fri Jan 15 00:23:31 2021 +0100 [IO-701] Make PathUtils.setReadOnly deal with LinuxDosFileAttributeView. (#186) Add FileUtilsDeleteDirectory*TestCases. Use java.nio.file.Files.delete for FileUtils.delete. --- src/main/java/org/apache/commons/io/FileUtils.java | 4 +- .../java/org/apache/commons/io/file/PathUtils.java | 21 +- .../io/FileUtilsDeleteDirectoryBaseTestCase.java | 233 +++++++++++++++++++++ .../io/FileUtilsDeleteDirectoryLinuxTestCase.java | 120 +++++++++++ .../io/FileUtilsDeleteDirectoryWinTestCase.java | 50 +++++ 5 files changed, 422 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java index adc9261..7df6290 100644 --- a/src/main/java/org/apache/commons/io/FileUtils.java +++ b/src/main/java/org/apache/commons/io/FileUtils.java @@ -1167,9 +1167,7 @@ public class FileUtils { */ public static File delete(final File file) throws IOException { Objects.requireNonNull(file, "file"); - if (!file.delete()) { - throw new IOException("Unable to delete " + file); - } + java.nio.file.Files.delete(file.toPath()); return file; } 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 87f605a..9cb1ec1 100644 --- a/src/main/java/org/apache/commons/io/file/PathUtils.java +++ b/src/main/java/org/apache/commons/io/file/PathUtils.java @@ -41,6 +41,7 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -53,6 +54,7 @@ import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.io.IOExceptionList; import org.apache.commons.io.IOUtils; import org.apache.commons.io.file.Counters.PathCounters; import org.apache.commons.io.filefilter.IOFileFilter; @@ -869,11 +871,17 @@ public final class PathUtils { */ public static Path setReadOnly(final Path path, final boolean readOnly, final LinkOption... linkOptions) throws IOException { + final List<Exception> causeList = new ArrayList<>(2); final DosFileAttributeView fileAttributeView = Files.getFileAttributeView(path, DosFileAttributeView.class, linkOptions); if (fileAttributeView != null) { - fileAttributeView.setReadOnly(readOnly); - return path; + try { + fileAttributeView.setReadOnly(readOnly); + return path; + } catch (IOException e) { + //ignore for now, retry with PosixFileAttributeView + causeList.add(e); + } } final PosixFileAttributeView posixFileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class, linkOptions); @@ -886,7 +894,14 @@ public final class PathUtils { permissions.remove(PosixFilePermission.OWNER_WRITE); permissions.remove(PosixFilePermission.GROUP_WRITE); permissions.remove(PosixFilePermission.OTHERS_WRITE); - return Files.setPosixFilePermissions(path, permissions); + try { + return Files.setPosixFilePermissions(path, permissions); + } catch (IOException e) { + causeList.add(e); + } + } + if (!causeList.isEmpty()) { + throw new IOExceptionList(causeList); } throw new IOException( String.format("No DosFileAttributeView or PosixFileAttributeView for '%s' (linkOptions=%s)", path, diff --git a/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryBaseTestCase.java b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryBaseTestCase.java new file mode 100644 index 0000000..3aacbe8 --- /dev/null +++ b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryBaseTestCase.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.attribute.FileAttribute; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Test cases for FileUtils.deleteDirectory() method. + * + */ +public abstract class FileUtilsDeleteDirectoryBaseTestCase { + @TempDir + public File top; + + // ----------------------------------------------------------------------- + + @Test + public void testDeletesRegular() throws Exception { + final File nested = new File(top, "nested"); + assertTrue(nested.mkdirs()); + + assertEquals(1, top.list().length); + + assertEquals(0, nested.list().length); + + FileUtils.deleteDirectory(nested); + + assertEquals(0, top.list().length); + } + + @Test + public void testDeletesNested() throws Exception { + final File nested = new File(top, "nested"); + assertTrue(nested.mkdirs()); + + assertEquals(1, top.list().length); + + FileUtils.touch(new File(nested, "regular")); + FileUtils.touch(new File(nested, ".hidden")); + + assertEquals(2, nested.list().length); + + FileUtils.deleteDirectory(nested); + + assertEquals(0, top.list().length); + } + + @Test + public void testDeleteDirWithSymlinkFile() throws Exception { + final File realOuter = new File(top, "realouter"); + assertTrue(realOuter.mkdirs()); + + final File realInner = new File(realOuter, "realinner"); + assertTrue(realInner.mkdirs()); + + final File realFile = new File(realInner, "file1"); + FileUtils.touch(realFile); + + assertEquals(1, realInner.list().length); + + final File randomFile = new File(top, "randomfile"); + FileUtils.touch(randomFile); + + final File symlinkFile = new File(realInner, "fakeinner"); + assertTrue(setupSymlink(randomFile, symlinkFile)); + + assertEquals(2, realInner.list().length); + assertEquals(2, top.list().length); + + // assert the real directory were removed including the symlink + FileUtils.deleteDirectory(realOuter); + assertEquals(1, top.list().length); + + // ensure that the contents of the symlink were NOT removed. + assertTrue(randomFile.exists()); + assertFalse(symlinkFile.exists()); + } + + @Test + public void testDeleteDirWithASymlinkDir() throws Exception { + + final File realOuter = new File(top, "realouter"); + assertTrue(realOuter.mkdirs()); + + final File realInner = new File(realOuter, "realinner"); + assertTrue(realInner.mkdirs()); + + FileUtils.touch(new File(realInner, "file1")); + assertEquals(1, realInner.list().length); + + final File randomDirectory = new File(top, "randomDir"); + assertTrue(randomDirectory.mkdirs()); + + FileUtils.touch(new File(randomDirectory, "randomfile")); + assertEquals(1, randomDirectory.list().length); + + final File symlinkDirectory = new File(realOuter, "fakeinner"); + assertTrue(setupSymlink(randomDirectory, symlinkDirectory)); + + assertEquals(1, symlinkDirectory.list().length); + + // assert contents of the real directory were removed including the symlink + FileUtils.deleteDirectory(realOuter); + assertEquals(1, top.list().length); + + // ensure that the contents of the symlink were NOT removed. + assertEquals(1, randomDirectory.list().length, "Contents of sym link should not have been removed"); + } + + @Test + public void testDeleteDirWithASymlinkDir2() throws Exception { + + final File realOuter = new File(top, "realouter"); + assertTrue(realOuter.mkdirs()); + + final File realInner = new File(realOuter, "realinner"); + assertTrue(realInner.mkdirs()); + + FileUtils.touch(new File(realInner, "file1")); + assertEquals(1, realInner.list().length); + + final File randomDirectory = new File(top, "randomDir"); + assertTrue(randomDirectory.mkdirs()); + + FileUtils.touch(new File(randomDirectory, "randomfile")); + assertEquals(1, randomDirectory.list().length); + + final File symlinkDirectory = new File(realOuter, "fakeinner"); + Files.createSymbolicLink(symlinkDirectory.toPath(), randomDirectory.toPath()); + + assertEquals(1, symlinkDirectory.list().length); + + // assert contents of the real directory were removed including the symlink + FileUtils.deleteDirectory(realOuter); + assertEquals(1, top.list().length); + + // ensure that the contents of the symlink were NOT removed. + assertEquals(1, randomDirectory.list().length, "Contents of sym link should not have been removed"); + } + + @Test + public void testDeleteParentSymlink() throws Exception { + final File realParent = new File(top, "realparent"); + assertTrue(realParent.mkdirs()); + + final File realInner = new File(realParent, "realinner"); + assertTrue(realInner.mkdirs()); + + FileUtils.touch(new File(realInner, "file1")); + assertEquals(1, realInner.list().length); + + final File randomDirectory = new File(top, "randomDir"); + assertTrue(randomDirectory.mkdirs()); + + FileUtils.touch(new File(randomDirectory, "randomfile")); + assertEquals(1, randomDirectory.list().length); + + final File symlinkDirectory = new File(realParent, "fakeinner"); + assertTrue(setupSymlink(randomDirectory, symlinkDirectory)); + + assertEquals(1, symlinkDirectory.list().length); + + final File symlinkParentDirectory = new File(top, "fakeouter"); + assertTrue(setupSymlink(realParent, symlinkParentDirectory)); + + // assert only the symlink is deleted, but not followed + FileUtils.deleteDirectory(symlinkParentDirectory); + assertEquals(2, top.list().length); + + // ensure that the contents of the symlink were NOT removed. + assertEquals(1, randomDirectory.list().length, "Contents of sym link should not have been removed"); + } + + @Test + public void testDeleteParentSymlink2() throws Exception { + final File realParent = new File(top, "realparent"); + assertTrue(realParent.mkdirs()); + + final File realInner = new File(realParent, "realinner"); + assertTrue(realInner.mkdirs()); + + FileUtils.touch(new File(realInner, "file1")); + assertEquals(1, realInner.list().length); + + final File randomDirectory = new File(top, "randomDir"); + assertTrue(randomDirectory.mkdirs()); + + FileUtils.touch(new File(randomDirectory, "randomfile")); + assertEquals(1, randomDirectory.list().length); + + final File symlinkDirectory = new File(realParent, "fakeinner"); + Files.createSymbolicLink(symlinkDirectory.toPath(), randomDirectory.toPath()); + + assertEquals(1, symlinkDirectory.list().length); + + final File symlinkParentDirectory = new File(top, "fakeouter"); + Files.createSymbolicLink(symlinkParentDirectory.toPath(), realParent.toPath()); + + // assert only the symlink is deleted, but not followed + FileUtils.deleteDirectory(symlinkParentDirectory); + assertEquals(2, top.list().length); + + // ensure that the contents of the symlink were NOT removed. + assertEquals(1, randomDirectory.list().length, "Contents of sym link should not have been removed"); + } + + protected abstract boolean setupSymlink(final File res, final File link) throws Exception; + +} + diff --git a/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryLinuxTestCase.java b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryLinuxTestCase.java new file mode 100644 index 0000000..ad54483 --- /dev/null +++ b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryLinuxTestCase.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +@DisabledOnOs(OS.WINDOWS) +public class FileUtilsDeleteDirectoryLinuxTestCase extends FileUtilsDeleteDirectoryBaseTestCase { + + @Test + public void testThrowsOnNullList() throws Exception { + final File nested = new File(top, "nested"); + assertTrue(nested.mkdirs()); + + // test wont work if we can't restrict permissions on the + // directory, so skip it. + assumeTrue(chmod(nested, 0, false)); + + try { + // cleanDirectory calls forceDelete + FileUtils.deleteDirectory(nested); + fail("expected IOException"); + } catch (final IOException e) { + assertEquals("Unknown I/O error listing contents of directory: " + nested.getAbsolutePath(), + e.getMessage()); + } finally { + chmod(nested, 755, false); + FileUtils.deleteDirectory(nested); + } + assertEquals(0, top.list().length); + } + + @Test + public void testThrowsOnCannotDeleteFile() throws Exception { + final File nested = new File(top, "nested"); + assertTrue(nested.mkdirs()); + + final File file = new File(nested, "restricted"); + FileUtils.touch(file); + + assumeTrue(chmod(nested, 500, false)); + + try { + // deleteDirectory calls forceDelete + FileUtils.deleteDirectory(nested); + fail("expected IOException"); + } catch (final IOException e) { + final IOExceptionList list = (IOExceptionList) e; + assertEquals("Cannot delete file: " + file.getAbsolutePath(), list.getCause(0).getMessage()); + } finally { + chmod(nested, 755, false); + FileUtils.deleteDirectory(nested); + } + assertEquals(0, top.list().length); + } + + @Override + protected boolean setupSymlink(File res, File link) throws Exception { + // create symlink + final List<String> args = new ArrayList<>(); + args.add("ln"); + args.add("-s"); + + args.add(res.getAbsolutePath()); + args.add(link.getAbsolutePath()); + + final Process proc; + + proc = Runtime.getRuntime().exec(args.toArray(new String[args.size()])); + return proc.waitFor() == 0; + } + + /** Only runs on Linux. */ + private boolean chmod(final File file, final int mode, final boolean recurse) throws InterruptedException { + final List<String> args = new ArrayList<>(); + args.add("chmod"); + + if (recurse) { + args.add("-R"); + } + + args.add(Integer.toString(mode)); + args.add(file.getAbsolutePath()); + + final Process proc; + + try { + proc = Runtime.getRuntime().exec(args.toArray(new String[args.size()])); + } catch (final IOException e) { + return false; + } + return proc.waitFor() == 0; + } +} diff --git a/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryWinTestCase.java b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryWinTestCase.java new file mode 100644 index 0000000..4ed4eda --- /dev/null +++ b/src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryWinTestCase.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +@EnabledOnOs(OS.WINDOWS) +public class FileUtilsDeleteDirectoryWinTestCase extends FileUtilsDeleteDirectoryBaseTestCase { + + @Override + protected boolean setupSymlink(File res, File link) throws Exception { + // create symlink + final List<String> args = new ArrayList<>(); + args.add("cmd"); + args.add("/C"); + args.add("mklink"); + + if (res.isDirectory()) { + args.add("/D"); + } + + args.add(link.getAbsolutePath()); + args.add(res.getAbsolutePath()); + + final Process proc; + + proc = Runtime.getRuntime().exec(args.toArray(new String[args.size()])); + return proc.waitFor() == 0; + } + +}