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 * http://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 java.io.IOException; 21 import java.nio.file.FileVisitResult; 22 import java.nio.file.Files; 23 import java.nio.file.LinkOption; 24 import java.nio.file.NoSuchFileException; 25 import java.nio.file.Path; 26 import java.nio.file.attribute.BasicFileAttributes; 27 import java.util.Arrays; 28 import java.util.Objects; 29 30 import org.apache.commons.io.file.Counters.PathCounters; 31 32 /** 33 * Deletes files and directories as a visit proceeds. 34 * 35 * @since 2.7 36 */ 37 public class DeletingPathVisitor extends CountingPathVisitor { 38 39 /** 40 * Constructs a new instance configured with a BigInteger {@link PathCounters}. 41 * 42 * @return a new instance configured with a BigInteger {@link PathCounters}. 43 */ withBigIntegerCounters()44 public static DeletingPathVisitor withBigIntegerCounters() { 45 return new DeletingPathVisitor(Counters.bigIntegerPathCounters()); 46 } 47 48 /** 49 * Constructs a new instance configured with a long {@link PathCounters}. 50 * 51 * @return a new instance configured with a long {@link PathCounters}. 52 */ withLongCounters()53 public static DeletingPathVisitor withLongCounters() { 54 return new DeletingPathVisitor(Counters.longPathCounters()); 55 } 56 57 private final String[] skip; 58 private final boolean overrideReadOnly; 59 private final LinkOption[] linkOptions; 60 61 /** 62 * Constructs a new visitor that deletes files except for the files and directories explicitly given. 63 * 64 * @param pathCounter How to count visits. 65 * @param deleteOption How deletion is handled. 66 * @param skip The files to skip deleting. 67 * @since 2.8.0 68 */ DeletingPathVisitor(final PathCounters pathCounter, final DeleteOption[] deleteOption, final String... skip)69 public DeletingPathVisitor(final PathCounters pathCounter, final DeleteOption[] deleteOption, final String... skip) { 70 this(pathCounter, PathUtils.noFollowLinkOptionArray(), deleteOption, skip); 71 } 72 73 /** 74 * Constructs a new visitor that deletes files except for the files and directories explicitly given. 75 * 76 * @param pathCounter How to count visits. 77 * @param linkOptions How symbolic links are handled. 78 * @param deleteOption How deletion is handled. 79 * @param skip The files to skip deleting. 80 * @since 2.9.0 81 */ DeletingPathVisitor(final PathCounters pathCounter, final LinkOption[] linkOptions, final DeleteOption[] deleteOption, final String... skip)82 public DeletingPathVisitor(final PathCounters pathCounter, final LinkOption[] linkOptions, final DeleteOption[] deleteOption, final String... skip) { 83 super(pathCounter); 84 final String[] temp = skip != null ? skip.clone() : EMPTY_STRING_ARRAY; 85 Arrays.sort(temp); 86 this.skip = temp; 87 this.overrideReadOnly = StandardDeleteOption.overrideReadOnly(deleteOption); 88 // TODO Files.deleteIfExists() never follows links, so use LinkOption.NOFOLLOW_LINKS in other calls to Files. 89 this.linkOptions = linkOptions == null ? PathUtils.noFollowLinkOptionArray() : linkOptions.clone(); 90 } 91 92 /** 93 * Constructs a new visitor that deletes files except for the files and directories explicitly given. 94 * 95 * @param pathCounter How to count visits. 96 * 97 * @param skip The files to skip deleting. 98 */ DeletingPathVisitor(final PathCounters pathCounter, final String... skip)99 public DeletingPathVisitor(final PathCounters pathCounter, final String... skip) { 100 this(pathCounter, PathUtils.EMPTY_DELETE_OPTION_ARRAY, skip); 101 } 102 103 /** 104 * Returns true to process the given path, false if not. 105 * 106 * @param path the path to test. 107 * @return true to process the given path, false if not. 108 */ accept(final Path path)109 private boolean accept(final Path path) { 110 return Arrays.binarySearch(skip, Objects.toString(path.getFileName(), null)) < 0; 111 } 112 113 @Override equals(final Object obj)114 public boolean equals(final Object obj) { 115 if (this == obj) { 116 return true; 117 } 118 if (!super.equals(obj)) { 119 return false; 120 } 121 if (getClass() != obj.getClass()) { 122 return false; 123 } 124 final DeletingPathVisitor other = (DeletingPathVisitor) obj; 125 return overrideReadOnly == other.overrideReadOnly && Arrays.equals(skip, other.skip); 126 } 127 128 @Override hashCode()129 public int hashCode() { 130 final int prime = 31; 131 int result = super.hashCode(); 132 result = prime * result + Arrays.hashCode(skip); 133 result = prime * result + Objects.hash(overrideReadOnly); 134 return result; 135 } 136 137 @Override postVisitDirectory(final Path dir, final IOException exc)138 public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException { 139 if (PathUtils.isEmptyDirectory(dir)) { 140 Files.deleteIfExists(dir); 141 } 142 return super.postVisitDirectory(dir, exc); 143 } 144 145 @Override preVisitDirectory(final Path dir, final BasicFileAttributes attrs)146 public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException { 147 super.preVisitDirectory(dir, attrs); 148 return accept(dir) ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE; 149 } 150 151 @Override visitFile(final Path file, final BasicFileAttributes attrs)152 public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { 153 if (accept(file)) { 154 // delete files and valid links, respecting linkOptions 155 if (Files.exists(file, linkOptions)) { 156 if (overrideReadOnly) { 157 PathUtils.setReadOnly(file, false, linkOptions); 158 } 159 Files.deleteIfExists(file); 160 } 161 // invalid links will survive previous delete, different approach needed: 162 if (Files.isSymbolicLink(file)) { 163 try { 164 // deleteIfExists does not work for this case 165 Files.delete(file); 166 } catch (final NoSuchFileException ignored) { 167 // ignore 168 } 169 } 170 } 171 updateFileCounters(file, attrs); 172 return FileVisitResult.CONTINUE; 173 } 174 }