You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2021/01/14 23:23:42 UTC

[commons-io] branch master updated: [IO-701] Make PathUtils.setReadOnly deal with LinuxDosFileAttributeView. (#186)

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...@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;
+	}
+
+}