Make PathUtils.setReadOnly deal with LinuxDosFileAttributeView.
Add FileUtilsDeleteDirectory*TestCases. Use java.nio.file.Files.delete for FileUtils.delete.
MODIFY
src/main/java/org/apache/commons/io/FileUtils.java
src/main/java/org/apache/commons/io/FileUtils.java
@@ -1167,9 +1167,7 @@ static String decodeUrl(final String url) {
*/
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;
}

MODIFY
src/main/java/org/apache/commons/io/file/PathUtils.java
src/main/java/org/apache/commons/io/file/PathUtils.java
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;
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 static BasicFileAttributes readBasicFileAttributesQuietly(final Path path
*/
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) {
+ 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 static Path setReadOnly(final Path path, final boolean readOnly, final Li
permissions.remove(PosixFilePermission.OWNER_WRITE);
permissions.remove(PosixFilePermission.GROUP_WRITE);
permissions.remove(PosixFilePermission.OTHERS_WRITE);
+ 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,

ADD
src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryBaseTestCase.java
+/*
+ * 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;
+
+}
+

ADD
src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryLinuxTestCase.java
+/*
+ * 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;
+ }
+}

ADD
src/test/java/org/apache/commons/io/FileUtilsDeleteDirectoryWinTestCase.java
+/*
+ * 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;
+ }
+
+}

[IO-701] Make PathUtils.setReadOnly deal with LinuxDosFileAttributeView. (#186)

Add FileUtilsDeleteDirectory*TestCases. Use java.nio.file.Files.delete for FileUtils.delete.