Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2014 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | package com.google.devtools.build.lib.vfs; |
| 15 | |
| 16 | import static java.nio.charset.StandardCharsets.ISO_8859_1; |
| 17 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 18 | import com.google.common.base.Predicate; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 19 | import com.google.common.io.ByteSink; |
| 20 | import com.google.common.io.ByteSource; |
| 21 | import com.google.common.io.ByteStreams; |
| 22 | import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe; |
| 23 | import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; |
Mark Schaller | 6df8179 | 2015-12-10 18:47:47 +0000 | [diff] [blame] | 24 | import com.google.devtools.build.lib.util.Preconditions; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 25 | import java.io.IOException; |
| 26 | import java.io.InputStream; |
| 27 | import java.io.OutputStream; |
| 28 | import java.io.PrintStream; |
| 29 | import java.nio.charset.Charset; |
| 30 | import java.util.ArrayList; |
| 31 | import java.util.Arrays; |
| 32 | import java.util.Collection; |
| 33 | import java.util.List; |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 34 | import java.util.Set; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 35 | |
| 36 | /** |
| 37 | * Helper functions that implement often-used complex operations on file |
| 38 | * systems. |
| 39 | */ |
| 40 | @ConditionallyThreadSafe // ThreadSafe except for deleteTree. |
| 41 | public class FileSystemUtils { |
| 42 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 43 | private FileSystemUtils() {} |
| 44 | |
| 45 | /**************************************************************************** |
| 46 | * Path and PathFragment functions. |
| 47 | */ |
| 48 | |
| 49 | /** |
| 50 | * Throws exceptions if {@code baseName} is not a valid base name. A valid |
| 51 | * base name: |
| 52 | * <ul> |
| 53 | * <li>Is not null |
| 54 | * <li>Is not an empty string |
| 55 | * <li>Is not "." or ".." |
| 56 | * <li>Does not contain a slash |
| 57 | * </ul> |
| 58 | */ |
| 59 | @ThreadSafe |
| 60 | public static void checkBaseName(String baseName) { |
| 61 | if (baseName.length() == 0) { |
| 62 | throw new IllegalArgumentException("Child must not be empty string ('')"); |
| 63 | } |
| 64 | if (baseName.equals(".") || baseName.equals("..")) { |
| 65 | throw new IllegalArgumentException("baseName must not be '" + baseName + "'"); |
| 66 | } |
| 67 | if (baseName.indexOf('/') != -1) { |
| 68 | throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'"); |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Returns the common ancestor between two paths, or null if none (including |
| 74 | * if they are on different filesystems). |
| 75 | */ |
| 76 | public static Path commonAncestor(Path a, Path b) { |
| 77 | while (a != null && !b.startsWith(a)) { |
| 78 | a = a.getParentDirectory(); // returns null at root |
| 79 | } |
| 80 | return a; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Returns the longest common ancestor of the two path fragments, or either "/" or "" (depending |
| 85 | * on whether {@code a} is absolute or relative) if there is none. |
| 86 | */ |
| 87 | public static PathFragment commonAncestor(PathFragment a, PathFragment b) { |
| 88 | while (a != null && !b.startsWith(a)) { |
| 89 | a = a.getParentDirectory(); |
| 90 | } |
| 91 | |
| 92 | return a; |
| 93 | } |
| 94 | /** |
| 95 | * Returns a path fragment from a given from-dir to a given to-path. May be |
| 96 | * either a short relative path "foo/bar", an up'n'over relative path |
| 97 | * "../../foo/bar" or an absolute path. |
| 98 | */ |
| 99 | public static PathFragment relativePath(Path fromDir, Path to) { |
| 100 | if (to.getFileSystem() != fromDir.getFileSystem()) { |
| 101 | throw new IllegalArgumentException("fromDir and to must be on the same FileSystem"); |
| 102 | } |
| 103 | |
| 104 | return relativePath(fromDir.asFragment(), to.asFragment()); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Returns a path fragment from a given from-dir to a given to-path. |
| 109 | */ |
| 110 | public static PathFragment relativePath(PathFragment fromDir, PathFragment to) { |
| 111 | if (to.equals(fromDir)) { |
| 112 | return new PathFragment("."); // same dir, just return '.' |
| 113 | } |
| 114 | if (to.startsWith(fromDir)) { |
| 115 | return to.relativeTo(fromDir); // easy case--it's a descendant |
| 116 | } |
| 117 | PathFragment ancestor = commonAncestor(fromDir, to); |
| 118 | if (ancestor == null) { |
| 119 | return to; // no common ancestor, use 'to' |
| 120 | } |
| 121 | int levels = fromDir.relativeTo(ancestor).segmentCount(); |
| 122 | StringBuilder dotdots = new StringBuilder(); |
| 123 | for (int i = 0; i < levels; i++) { |
| 124 | dotdots.append("../"); |
| 125 | } |
| 126 | return new PathFragment(dotdots.toString()).getRelative(to.relativeTo(ancestor)); |
| 127 | } |
| 128 | |
| 129 | /** |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 130 | * Removes the shortest suffix beginning with '.' from the basename of the |
| 131 | * filename string. If the basename contains no '.', the filename is returned |
| 132 | * unchanged. |
| 133 | * |
Janak Ramakrishnan | 525019b | 2015-07-08 17:05:43 +0000 | [diff] [blame] | 134 | * <p>e.g. "foo/bar.x" -> "foo/bar" |
| 135 | * |
| 136 | * <p>Note that if the filename is composed entirely of ".", this method will return the string |
| 137 | * with one fewer ".", which may have surprising effects. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 138 | */ |
| 139 | @ThreadSafe |
| 140 | public static String removeExtension(String filename) { |
| 141 | int lastDotIndex = filename.lastIndexOf('.'); |
| 142 | if (lastDotIndex == -1) { return filename; } |
| 143 | int lastSlashIndex = filename.lastIndexOf('/'); |
| 144 | if (lastSlashIndex > lastDotIndex) { |
| 145 | return filename; |
| 146 | } |
| 147 | return filename.substring(0, lastDotIndex); |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Removes the shortest suffix beginning with '.' from the basename of the |
| 152 | * PathFragment. If the basename contains no '.', the filename is returned |
| 153 | * unchanged. |
| 154 | * |
| 155 | * <p>e.g. "foo/bar.x" -> "foo/bar" |
Janak Ramakrishnan | 525019b | 2015-07-08 17:05:43 +0000 | [diff] [blame] | 156 | * |
| 157 | * <p>Note that if the base filename is composed entirely of ".", this method will return the |
| 158 | * filename with one fewer "." in the base filename, which may have surprising effects. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 159 | */ |
| 160 | @ThreadSafe |
| 161 | public static PathFragment removeExtension(PathFragment path) { |
| 162 | return path.replaceName(removeExtension(path.getBaseName())); |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Removes the shortest suffix beginning with '.' from the basename of the |
| 167 | * Path. If the basename contains no '.', the filename is returned |
| 168 | * unchanged. |
| 169 | * |
| 170 | * <p>e.g. "foo/bar.x" -> "foo/bar" |
Janak Ramakrishnan | 525019b | 2015-07-08 17:05:43 +0000 | [diff] [blame] | 171 | * |
| 172 | * <p>Note that if the base filename is composed entirely of ".", this method will return the |
| 173 | * filename with one fewer "." in the base filename, which may have surprising effects. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 174 | */ |
| 175 | @ThreadSafe |
| 176 | public static Path removeExtension(Path path) { |
| 177 | return path.getFileSystem().getPath(removeExtension(path.asFragment())); |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Returns a new {@code PathFragment} formed by replacing the extension of the |
| 182 | * last path segment of {@code path} with {@code newExtension}. Null is |
| 183 | * returned iff {@code path} has zero segments. |
| 184 | */ |
| 185 | public static PathFragment replaceExtension(PathFragment path, String newExtension) { |
| 186 | return path.replaceName(removeExtension(path.getBaseName()) + newExtension); |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Returns a new {@code PathFragment} formed by replacing the extension of the |
| 191 | * last path segment of {@code path} with {@code newExtension}. Null is |
| 192 | * returned iff {@code path} has zero segments or it doesn't end with {@code oldExtension}. |
| 193 | */ |
| 194 | public static PathFragment replaceExtension(PathFragment path, String newExtension, |
| 195 | String oldExtension) { |
| 196 | String base = path.getBaseName(); |
| 197 | if (!base.endsWith(oldExtension)) { |
| 198 | return null; |
| 199 | } |
| 200 | String newBase = base.substring(0, base.length() - oldExtension.length()) + newExtension; |
| 201 | return path.replaceName(newBase); |
| 202 | } |
| 203 | |
| 204 | /** |
| 205 | * Returns a new {@code Path} formed by replacing the extension of the |
| 206 | * last path segment of {@code path} with {@code newExtension}. Null is |
| 207 | * returned iff {@code path} has zero segments. |
| 208 | */ |
| 209 | public static Path replaceExtension(Path path, String newExtension) { |
| 210 | PathFragment fragment = replaceExtension(path.asFragment(), newExtension); |
| 211 | return fragment == null ? null : path.getFileSystem().getPath(fragment); |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Returns a new {@code PathFragment} formed by adding the extension to the last path segment of |
| 216 | * {@code path}. Null is returned if {@code path} has zero segments. |
| 217 | */ |
| 218 | public static PathFragment appendExtension(PathFragment path, String newExtension) { |
| 219 | return path.replaceName(path.getBaseName() + newExtension); |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | * Returns a new {@code PathFragment} formed by replacing the first, or all if |
| 224 | * {@code replaceAll} is true, {@code oldSegment} of {@code path} with {@code |
| 225 | * newSegment}. |
| 226 | */ |
| 227 | public static PathFragment replaceSegments(PathFragment path, |
| 228 | String oldSegment, String newSegment, boolean replaceAll) { |
| 229 | int count = path.segmentCount(); |
| 230 | for (int i = 0; i < count; i++) { |
| 231 | if (path.getSegment(i).equals(oldSegment)) { |
| 232 | path = new PathFragment(path.subFragment(0, i), |
| 233 | new PathFragment(newSegment), |
| 234 | path.subFragment(i+1, count)); |
| 235 | if (!replaceAll) { |
| 236 | return path; |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | return path; |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Returns a new {@code PathFragment} formed by appending the given string to the last path |
| 245 | * segment of {@code path} without removing the extension. Returns null if {@code path} |
| 246 | * has zero segments. |
| 247 | */ |
| 248 | public static PathFragment appendWithoutExtension(PathFragment path, String toAppend) { |
| 249 | return path.replaceName(appendWithoutExtension(path.getBaseName(), toAppend)); |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Given a string that represents a file with an extension separated by a '.' and a string |
| 254 | * to append, return a string in which {@code toAppend} has been appended to {@code name} |
| 255 | * before the last '.' character. If {@code name} does not include a '.', appends {@code |
| 256 | * toAppend} at the end. |
| 257 | * |
| 258 | * <p>For example, |
| 259 | * ("libfoo.jar", "-src") ==> "libfoo-src.jar" |
| 260 | * ("libfoo", "-src") ==> "libfoo-src" |
| 261 | */ |
| 262 | private static String appendWithoutExtension(String name, String toAppend) { |
Ulf Adams | 07dba94 | 2015-03-05 14:47:37 +0000 | [diff] [blame] | 263 | int dotIndex = name.lastIndexOf('.'); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 264 | if (dotIndex > 0) { |
| 265 | String baseName = name.substring(0, dotIndex); |
| 266 | String extension = name.substring(dotIndex); |
| 267 | return baseName + toAppend + extension; |
| 268 | } else { |
| 269 | return name + toAppend; |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | /**************************************************************************** |
| 274 | * FileSystem property functions. |
| 275 | */ |
| 276 | |
| 277 | /** |
| 278 | * Return the current working directory as expressed by the System property |
| 279 | * 'user.dir'. |
| 280 | */ |
| 281 | public static Path getWorkingDirectory(FileSystem fs) { |
| 282 | return fs.getPath(getWorkingDirectory()); |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Returns the current working directory as expressed by the System property |
| 287 | * 'user.dir'. This version does not require a {@link FileSystem}. |
| 288 | */ |
| 289 | public static PathFragment getWorkingDirectory() { |
| 290 | return new PathFragment(System.getProperty("user.dir", "/")); |
| 291 | } |
| 292 | |
| 293 | /**************************************************************************** |
| 294 | * Path FileSystem mutating operations. |
| 295 | */ |
| 296 | |
| 297 | /** |
| 298 | * "Touches" the file or directory specified by the path, following symbolic |
| 299 | * links. If it does not exist, it is created as an empty file; otherwise, the |
| 300 | * time of last access is updated to the current time. |
| 301 | * |
| 302 | * @throws IOException if there was an error while touching the file |
| 303 | */ |
| 304 | @ThreadSafe |
| 305 | public static void touchFile(Path path) throws IOException { |
| 306 | if (path.exists()) { |
| 307 | // -1L means "use the current time", and is ultimately implemented by |
| 308 | // utime(path, null), thereby using the kernel's clock, not the JVM's. |
| 309 | // (A previous implementation based on the JVM clock was found to be |
| 310 | // skewy.) |
| 311 | path.setLastModifiedTime(-1L); |
| 312 | } else { |
| 313 | createEmptyFile(path); |
| 314 | } |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * Creates an empty regular file with the name of the current path, following |
| 319 | * symbolic links. |
| 320 | * |
| 321 | * @throws IOException if the file could not be created for any reason |
| 322 | * (including that there was already a file at that location) |
| 323 | */ |
| 324 | public static void createEmptyFile(Path path) throws IOException { |
| 325 | path.getOutputStream().close(); |
| 326 | } |
| 327 | |
| 328 | /** |
| 329 | * Creates or updates a symbolic link from 'link' to 'target'. Replaces |
| 330 | * existing symbolic links with target, and skips the link creation if it is |
| 331 | * already present. Will also create any missing ancestor directories of the |
| 332 | * link. This method is non-atomic |
| 333 | * |
| 334 | * <p>Note: this method will throw an IOException if there is an unequal |
| 335 | * non-symlink at link. |
| 336 | * |
| 337 | * @throws IOException if the creation of the symbolic link was unsuccessful |
| 338 | * for any reason. |
| 339 | */ |
| 340 | @ThreadSafe // but not atomic |
| 341 | public static void ensureSymbolicLink(Path link, Path target) throws IOException { |
| 342 | ensureSymbolicLink(link, target.asFragment()); |
| 343 | } |
| 344 | |
| 345 | /** |
| 346 | * Creates or updates a symbolic link from 'link' to 'target'. Replaces |
| 347 | * existing symbolic links with target, and skips the link creation if it is |
| 348 | * already present. Will also create any missing ancestor directories of the |
| 349 | * link. This method is non-atomic |
| 350 | * |
| 351 | * <p>Note: this method will throw an IOException if there is an unequal |
| 352 | * non-symlink at link. |
| 353 | * |
| 354 | * @throws IOException if the creation of the symbolic link was unsuccessful |
| 355 | * for any reason. |
| 356 | */ |
| 357 | @ThreadSafe // but not atomic |
| 358 | public static void ensureSymbolicLink(Path link, String target) throws IOException { |
| 359 | ensureSymbolicLink(link, new PathFragment(target)); |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Creates or updates a symbolic link from 'link' to 'target'. Replaces |
| 364 | * existing symbolic links with target, and skips the link creation if it is |
| 365 | * already present. Will also create any missing ancestor directories of the |
| 366 | * link. This method is non-atomic |
| 367 | * |
| 368 | * <p>Note: this method will throw an IOException if there is an unequal |
| 369 | * non-symlink at link. |
| 370 | * |
| 371 | * @throws IOException if the creation of the symbolic link was unsuccessful |
| 372 | * for any reason. |
| 373 | */ |
| 374 | @ThreadSafe // but not atomic |
| 375 | public static void ensureSymbolicLink(Path link, PathFragment target) throws IOException { |
| 376 | // TODO(bazel-team): (2009) consider adding the logic for recovering from the case when |
| 377 | // we have already created a parent directory symlink earlier. |
| 378 | try { |
| 379 | if (link.readSymbolicLink().equals(target)) { |
| 380 | return; // Do nothing if the link is already there. |
| 381 | } |
| 382 | } catch (IOException e) { // link missing or broken |
| 383 | /* fallthru and do the work below */ |
| 384 | } |
| 385 | if (link.isSymbolicLink()) { |
| 386 | link.delete(); // Remove the symlink since it is pointing somewhere else. |
| 387 | } else { |
| 388 | createDirectoryAndParents(link.getParentDirectory()); |
| 389 | } |
| 390 | try { |
| 391 | link.createSymbolicLink(target); |
| 392 | } catch (IOException e) { |
| 393 | // Only pass on exceptions caused by a true link creation failure. |
| 394 | if (!link.isSymbolicLink() || |
| 395 | !link.resolveSymbolicLinks().equals(link.getRelative(target))) { |
| 396 | throw e; |
| 397 | } |
| 398 | } |
| 399 | } |
| 400 | |
Eric Fellheimer | 5843d1f | 2016-02-11 15:17:55 +0000 | [diff] [blame] | 401 | public static ByteSource asByteSource(final Path path) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 402 | return new ByteSource() { |
| 403 | @Override public InputStream openStream() throws IOException { |
| 404 | return path.getInputStream(); |
| 405 | } |
| 406 | }; |
| 407 | } |
| 408 | |
Eric Fellheimer | 5843d1f | 2016-02-11 15:17:55 +0000 | [diff] [blame] | 409 | public static ByteSink asByteSink(final Path path, final boolean append) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 410 | return new ByteSink() { |
| 411 | @Override public OutputStream openStream() throws IOException { |
| 412 | return path.getOutputStream(append); |
| 413 | } |
| 414 | }; |
| 415 | } |
| 416 | |
Eric Fellheimer | 5843d1f | 2016-02-11 15:17:55 +0000 | [diff] [blame] | 417 | public static ByteSink asByteSink(final Path path) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 418 | return asByteSink(path, false); |
| 419 | } |
| 420 | |
| 421 | /** |
| 422 | * Copies the file from location "from" to location "to", while overwriting a |
| 423 | * potentially existing "to". File's last modified time, executable and |
| 424 | * writable bits are also preserved. |
| 425 | * |
| 426 | * <p>If no error occurs, the method returns normally. If a parent directory does |
| 427 | * not exist, a FileNotFoundException is thrown. An IOException is thrown when |
| 428 | * other erroneous situations occur. (e.g. read errors) |
| 429 | */ |
| 430 | @ThreadSafe // but not atomic |
| 431 | public static void copyFile(Path from, Path to) throws IOException { |
| 432 | try { |
| 433 | to.delete(); |
| 434 | } catch (IOException e) { |
| 435 | throw new IOException("error copying file: " |
| 436 | + "couldn't delete destination: " + e.getMessage()); |
| 437 | } |
| 438 | asByteSource(from).copyTo(asByteSink(to)); |
| 439 | to.setLastModifiedTime(from.getLastModifiedTime()); // Preserve mtime. |
| 440 | if (!from.isWritable()) { |
| 441 | to.setWritable(false); // Make file read-only if original was read-only. |
| 442 | } |
| 443 | to.setExecutable(from.isExecutable()); // Copy executable bit. |
| 444 | } |
| 445 | |
| 446 | /** |
| 447 | * Copies a tool binary from one path to another, returning the target path. |
| 448 | * The directory of the target path must already exist. The target copy's time |
| 449 | * is set to match, as well as its read-only and executable flags. The |
| 450 | * operation is skipped if the target file has the same time and size as the |
| 451 | * source. |
| 452 | */ |
| 453 | public static Path copyTool(Path source, Path target) throws IOException { |
| 454 | FileStatus sourceStat = null; |
| 455 | FileStatus targetStat = target.statNullable(); |
| 456 | if (targetStat != null) { |
| 457 | // stat the source file only if we'll need the stat. |
| 458 | sourceStat = source.stat(Symlinks.FOLLOW); |
| 459 | } |
| 460 | if (targetStat == null || |
| 461 | targetStat.getLastModifiedTime() != sourceStat.getLastModifiedTime() || |
| 462 | targetStat.getSize() != sourceStat.getSize()) { |
| 463 | copyFile(source, target); |
| 464 | target.setWritable(source.isWritable()); |
| 465 | target.setExecutable(source.isExecutable()); |
| 466 | target.setLastModifiedTime(source.getLastModifiedTime()); |
| 467 | } |
| 468 | return target; |
| 469 | } |
| 470 | |
| 471 | /**************************************************************************** |
| 472 | * Directory tree operations. |
| 473 | */ |
| 474 | |
| 475 | /** |
| 476 | * Returns a new collection containing all of the paths below a given root |
| 477 | * path, for which the given predicate is true. Symbolic links are not |
| 478 | * followed, and may appear in the result. |
| 479 | * |
| 480 | * @throws IOException If the root does not denote a directory |
| 481 | */ |
| 482 | @ThreadSafe |
| 483 | public static Collection<Path> traverseTree(Path root, Predicate<? super Path> predicate) |
| 484 | throws IOException { |
| 485 | List<Path> paths = new ArrayList<>(); |
| 486 | traverseTree(paths, root, predicate); |
| 487 | return paths; |
| 488 | } |
| 489 | |
| 490 | /** |
| 491 | * Populates an existing Path List, adding all of the paths below a given root |
| 492 | * path for which the given predicate is true. Symbolic links are not |
| 493 | * followed, and may appear in the result. |
| 494 | * |
| 495 | * @throws IOException If the root does not denote a directory |
| 496 | */ |
| 497 | @ThreadSafe |
| 498 | public static void traverseTree(Collection<Path> paths, Path root, |
| 499 | Predicate<? super Path> predicate) throws IOException { |
| 500 | for (Path p : root.getDirectoryEntries()) { |
| 501 | if (predicate.apply(p)) { |
| 502 | paths.add(p); |
| 503 | } |
| 504 | if (p.isDirectory(Symlinks.NOFOLLOW)) { |
| 505 | traverseTree(paths, p, predicate); |
| 506 | } |
| 507 | } |
| 508 | } |
| 509 | |
| 510 | /** |
| 511 | * Deletes 'p', and everything recursively beneath it if it's a directory. |
| 512 | * Does not follow any symbolic links. |
| 513 | * |
| 514 | * @throws IOException if any file could not be removed. |
| 515 | */ |
| 516 | @ThreadSafe |
| 517 | public static void deleteTree(Path p) throws IOException { |
| 518 | deleteTreesBelow(p); |
| 519 | p.delete(); |
| 520 | } |
| 521 | |
| 522 | /** |
| 523 | * Deletes all dir trees recursively beneath 'dir' if it's a directory, |
| 524 | * nothing otherwise. Does not follow any symbolic links. |
| 525 | * |
| 526 | * @throws IOException if any file could not be removed. |
| 527 | */ |
| 528 | @ThreadSafe |
| 529 | public static void deleteTreesBelow(Path dir) throws IOException { |
| 530 | if (dir.isDirectory(Symlinks.NOFOLLOW)) { // real directories (not symlinks) |
| 531 | dir.setReadable(true); |
| 532 | dir.setWritable(true); |
| 533 | dir.setExecutable(true); |
| 534 | for (Path child : dir.getDirectoryEntries()) { |
| 535 | deleteTree(child); |
| 536 | } |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | /** |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 541 | * Copies all dir trees under a given 'from' dir to location 'to', while overwriting |
Lukacs Berki | 96202b1 | 2016-02-11 13:45:56 +0000 | [diff] [blame] | 542 | * all files in the potentially existing 'to'. Resolves symbolic links. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 543 | * |
| 544 | * <p>The source and the destination must be non-overlapping, otherwise an |
| 545 | * IllegalArgumentException will be thrown. This method cannot be used to copy |
| 546 | * a dir tree to a sub tree of itself. |
| 547 | * |
| 548 | * <p>If no error occurs, the method returns normally. If the given 'from' does |
| 549 | * not exist, a FileNotFoundException is thrown. An IOException is thrown when |
| 550 | * other erroneous situations occur. (e.g. read errors) |
| 551 | */ |
| 552 | @ThreadSafe |
| 553 | public static void copyTreesBelow(Path from , Path to) throws IOException { |
| 554 | if (to.startsWith(from)) { |
| 555 | throw new IllegalArgumentException(to + " is a subdirectory of " + from); |
| 556 | } |
| 557 | |
| 558 | Collection<Path> entries = from.getDirectoryEntries(); |
| 559 | for (Path entry : entries) { |
Lukacs Berki | 96202b1 | 2016-02-11 13:45:56 +0000 | [diff] [blame] | 560 | if (entry.isFile()) { |
| 561 | Path newEntry = to.getChild(entry.getBaseName()); |
| 562 | copyFile(entry, newEntry); |
| 563 | } else { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 564 | Path subDir = to.getChild(entry.getBaseName()); |
| 565 | subDir.createDirectory(); |
| 566 | copyTreesBelow(entry, subDir); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 567 | } |
| 568 | } |
| 569 | } |
| 570 | |
| 571 | /** |
| 572 | * Attempts to create a directory with the name of the given path, creating |
| 573 | * ancestors as necessary. |
| 574 | * |
| 575 | * <p>Postcondition: completes normally iff {@code dir} denotes an existing |
| 576 | * directory (not necessarily canonical); completes abruptly otherwise. |
| 577 | * |
| 578 | * @return true if the directory was successfully created anew, false if it |
| 579 | * already existed (including the case where {@code dir} denotes a symlink |
| 580 | * to an existing directory) |
| 581 | * @throws IOException if the directory could not be created |
| 582 | */ |
| 583 | @ThreadSafe |
| 584 | public static boolean createDirectoryAndParents(Path dir) throws IOException { |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 585 | return createDirectoryAndParentsWithCache(null, dir); |
| 586 | } |
| 587 | |
| 588 | /** |
| 589 | * Attempts to create a directory with the name of the given path, creating ancestors as |
| 590 | * necessary. Only creates directories or their parents if they are not contained in the set |
| 591 | * {@code createdDirs} and instead assumes that they already exist. This saves a round-trip to the |
| 592 | * kernel, but is only safe when no one deletes directories that have been created by this method. |
| 593 | * |
| 594 | * <p>Postcondition: completes normally iff {@code dir} denotes an existing directory (not |
| 595 | * necessarily canonical); completes abruptly otherwise. |
| 596 | * |
| 597 | * @return true if the directory was successfully created anew, false if it already existed |
| 598 | * (including the case where {@code dir} denotes a symlink to an existing directory) |
| 599 | * @throws IOException if the directory could not be created |
| 600 | */ |
| 601 | @ThreadSafe |
| 602 | public static boolean createDirectoryAndParentsWithCache(Set<Path> createdDirs, Path dir) |
| 603 | throws IOException { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 604 | // Optimised for minimal number of I/O calls. |
| 605 | |
| 606 | // Don't attempt to create the root directory. |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 607 | if (dir.getParentDirectory() == null) { |
| 608 | return false; |
| 609 | } |
| 610 | |
| 611 | // We already created that directory. |
| 612 | if (createdDirs != null && createdDirs.contains(dir)) { |
| 613 | return false; |
| 614 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 615 | |
| 616 | FileSystem filesystem = dir.getFileSystem(); |
| 617 | if (filesystem instanceof UnionFileSystem) { |
| 618 | // If using UnionFS, make sure that we do not traverse filesystem boundaries when creating |
| 619 | // parent directories by rehoming the path on the most specific filesystem. |
| 620 | FileSystem delegate = ((UnionFileSystem) filesystem).getDelegate(dir); |
| 621 | dir = delegate.getPath(dir.asFragment()); |
| 622 | } |
| 623 | |
| 624 | try { |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 625 | boolean result = dir.createDirectory(); |
| 626 | if (createdDirs != null) { |
| 627 | createdDirs.add(dir); |
| 628 | } |
| 629 | return result; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 630 | } catch (IOException e) { |
| 631 | if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 632 | createDirectoryAndParentsWithCache(createdDirs, dir.getParentDirectory()); |
| 633 | boolean result = dir.createDirectory(); |
| 634 | if (createdDirs != null) { |
| 635 | createdDirs.add(dir); |
| 636 | } |
| 637 | return result; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 638 | } else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST |
Philipp Wollermann | e219a24 | 2016-08-18 14:39:37 +0000 | [diff] [blame] | 639 | if (createdDirs != null) { |
| 640 | createdDirs.add(dir); |
| 641 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 642 | return false; |
| 643 | } else { |
| 644 | throw e; // some other error (e.g. ENOTDIR, EACCES, etc.) |
| 645 | } |
| 646 | } |
| 647 | } |
| 648 | |
| 649 | /** |
| 650 | * Attempts to remove a relative chain of directories under a given base. |
| 651 | * Returns {@code true} if the removal was successful, and returns {@code |
| 652 | * false} if the removal fails because a directory was not empty. An |
| 653 | * {@link IOException} is thrown for any other errors. |
| 654 | */ |
| 655 | @ThreadSafe |
| 656 | public static boolean removeDirectoryAndParents(Path base, PathFragment toRemove) { |
| 657 | if (toRemove.isAbsolute()) { |
| 658 | return false; |
| 659 | } |
| 660 | try { |
| 661 | for (; toRemove.segmentCount() > 0; toRemove = toRemove.getParentDirectory()) { |
| 662 | Path p = base.getRelative(toRemove); |
| 663 | if (p.exists()) { |
| 664 | p.delete(); |
| 665 | } |
| 666 | } |
| 667 | } catch (IOException e) { |
| 668 | return false; |
| 669 | } |
| 670 | return true; |
| 671 | } |
| 672 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 673 | /**************************************************************************** |
| 674 | * Whole-file I/O utilities for characters and bytes. These convenience |
| 675 | * methods are not efficient and should not be used for large amounts of data! |
| 676 | */ |
| 677 | |
Nathan Harmata | 4e69824 | 2015-10-20 23:18:23 +0000 | [diff] [blame] | 678 | /** |
| 679 | * Decodes the given byte array assumed to be encoded with ISO-8859-1 encoding (isolatin1). |
| 680 | */ |
| 681 | public static char[] convertFromLatin1(byte[] content) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 682 | char[] latin1 = new char[content.length]; |
| 683 | for (int i = 0; i < latin1.length; i++) { // yeah, latin1 is this easy! :-) |
| 684 | latin1[i] = (char) (0xff & content[i]); |
| 685 | } |
| 686 | return latin1; |
| 687 | } |
| 688 | |
| 689 | /** |
| 690 | * Writes lines to file using ISO-8859-1 encoding (isolatin1). |
| 691 | */ |
| 692 | @ThreadSafe // but not atomic |
| 693 | public static void writeIsoLatin1(Path file, String... lines) throws IOException { |
| 694 | writeLinesAs(file, ISO_8859_1, lines); |
| 695 | } |
| 696 | |
| 697 | /** |
| 698 | * Append lines to file using ISO-8859-1 encoding (isolatin1). |
| 699 | */ |
| 700 | @ThreadSafe // but not atomic |
| 701 | public static void appendIsoLatin1(Path file, String... lines) throws IOException { |
| 702 | appendLinesAs(file, ISO_8859_1, lines); |
| 703 | } |
| 704 | |
| 705 | /** |
| 706 | * Writes the specified String as ISO-8859-1 (latin1) encoded bytes to the |
| 707 | * file. Follows symbolic links. |
| 708 | * |
| 709 | * @throws IOException if there was an error |
| 710 | */ |
| 711 | public static void writeContentAsLatin1(Path outputFile, String content) throws IOException { |
| 712 | writeContent(outputFile, ISO_8859_1, content); |
| 713 | } |
| 714 | |
| 715 | /** |
| 716 | * Writes the specified String using the specified encoding to the file. |
| 717 | * Follows symbolic links. |
| 718 | * |
| 719 | * @throws IOException if there was an error |
| 720 | */ |
| 721 | public static void writeContent(Path outputFile, Charset charset, String content) |
| 722 | throws IOException { |
| 723 | asByteSink(outputFile).asCharSink(charset).write(content); |
| 724 | } |
| 725 | |
| 726 | /** |
| 727 | * Writes lines to file using the given encoding, ending every line with a |
| 728 | * line break '\n' character. |
| 729 | */ |
| 730 | @ThreadSafe // but not atomic |
| 731 | public static void writeLinesAs(Path file, Charset charset, String... lines) |
| 732 | throws IOException { |
| 733 | writeLinesAs(file, charset, Arrays.asList(lines)); |
| 734 | } |
| 735 | |
| 736 | /** |
| 737 | * Appends lines to file using the given encoding, ending every line with a |
| 738 | * line break '\n' character. |
| 739 | */ |
| 740 | @ThreadSafe // but not atomic |
| 741 | public static void appendLinesAs(Path file, Charset charset, String... lines) |
| 742 | throws IOException { |
| 743 | appendLinesAs(file, charset, Arrays.asList(lines)); |
| 744 | } |
| 745 | |
| 746 | /** |
| 747 | * Writes lines to file using the given encoding, ending every line with a |
| 748 | * line break '\n' character. |
| 749 | */ |
| 750 | @ThreadSafe // but not atomic |
| 751 | public static void writeLinesAs(Path file, Charset charset, Iterable<String> lines) |
| 752 | throws IOException { |
| 753 | createDirectoryAndParents(file.getParentDirectory()); |
| 754 | asByteSink(file).asCharSink(charset).writeLines(lines); |
| 755 | } |
| 756 | |
| 757 | /** |
| 758 | * Appends lines to file using the given encoding, ending every line with a |
| 759 | * line break '\n' character. |
| 760 | */ |
| 761 | @ThreadSafe // but not atomic |
| 762 | public static void appendLinesAs(Path file, Charset charset, Iterable<String> lines) |
| 763 | throws IOException { |
| 764 | createDirectoryAndParents(file.getParentDirectory()); |
| 765 | asByteSink(file, true).asCharSink(charset).writeLines(lines); |
| 766 | } |
| 767 | |
| 768 | /** |
| 769 | * Writes the specified byte array to the output file. Follows symbolic links. |
| 770 | * |
| 771 | * @throws IOException if there was an error |
| 772 | */ |
| 773 | public static void writeContent(Path outputFile, byte[] content) throws IOException { |
| 774 | asByteSink(outputFile).write(content); |
| 775 | } |
| 776 | |
| 777 | /** |
| 778 | * Returns the entirety of the specified input stream and returns it as a char |
| 779 | * array, decoding characters using ISO-8859-1 (Latin1). |
| 780 | * |
| 781 | * @throws IOException if there was an error |
| 782 | */ |
| 783 | public static char[] readContentAsLatin1(InputStream in) throws IOException { |
| 784 | return convertFromLatin1(ByteStreams.toByteArray(in)); |
| 785 | } |
| 786 | |
| 787 | /** |
| 788 | * Returns the entirety of the specified file and returns it as a char array, |
| 789 | * decoding characters using ISO-8859-1 (Latin1). |
| 790 | * |
| 791 | * @throws IOException if there was an error |
| 792 | */ |
| 793 | public static char[] readContentAsLatin1(Path inputFile) throws IOException { |
| 794 | return convertFromLatin1(readContent(inputFile)); |
| 795 | } |
| 796 | |
| 797 | /** |
| 798 | * Returns an iterable that allows iterating over ISO-8859-1 (Latin1) text |
| 799 | * file contents line by line. If the file ends in a line break, the iterator |
| 800 | * will return an empty string as the last element. |
| 801 | * |
| 802 | * @throws IOException if there was an error |
| 803 | */ |
| 804 | public static Iterable<String> iterateLinesAsLatin1(Path inputFile) throws IOException { |
Chris Parsons | 64fa351 | 2015-12-14 19:28:01 +0000 | [diff] [blame] | 805 | return readLines(inputFile, ISO_8859_1); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 806 | } |
| 807 | |
| 808 | /** |
Chris Parsons | 64fa351 | 2015-12-14 19:28:01 +0000 | [diff] [blame] | 809 | * Returns an iterable that allows iterating over text file contents line by line in the given |
| 810 | * {@link Charset}. If the file ends in a line break, the iterator will return an empty string |
| 811 | * as the last element. |
| 812 | * |
| 813 | * @throws IOException if there was an error |
| 814 | */ |
| 815 | public static Iterable<String> readLines(Path inputFile, Charset charset) throws IOException { |
| 816 | return asByteSource(inputFile).asCharSource(charset).readLines(); |
| 817 | } |
Luis Fernando Pino Duque | be10218 | 2016-05-23 14:03:55 +0000 | [diff] [blame] | 818 | |
Chris Parsons | 64fa351 | 2015-12-14 19:28:01 +0000 | [diff] [blame] | 819 | /** |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 820 | * Returns the entirety of the specified file and returns it as a byte array. |
| 821 | * |
| 822 | * @throws IOException if there was an error |
| 823 | */ |
| 824 | public static byte[] readContent(Path inputFile) throws IOException { |
| 825 | return asByteSource(inputFile).read(); |
| 826 | } |
| 827 | |
| 828 | /** |
Florian Weikert | 7503757 | 2015-08-01 20:03:46 +0000 | [diff] [blame] | 829 | * Reads the entire file using the given charset and returns the contents as a string |
| 830 | */ |
| 831 | public static String readContent(Path inputFile, Charset charset) throws IOException { |
| 832 | return asByteSource(inputFile).asCharSource(charset).read(); |
| 833 | } |
| 834 | |
| 835 | /** |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 836 | * Reads at most {@code limit} bytes from {@code inputFile} and returns it as a byte array. |
| 837 | * |
| 838 | * @throws IOException if there was an error. |
| 839 | */ |
| 840 | public static byte[] readContentWithLimit(Path inputFile, int limit) throws IOException { |
| 841 | Preconditions.checkArgument(limit >= 0, "limit needs to be >=0, but it is %s", limit); |
| 842 | ByteSource byteSource = asByteSource(inputFile); |
| 843 | byte[] buffer = new byte[limit]; |
| 844 | try (InputStream inputStream = byteSource.openBufferedStream()) { |
| 845 | int read = ByteStreams.read(inputStream, buffer, 0, limit); |
Eric Fellheimer | 7f68994 | 2015-11-04 01:20:00 +0000 | [diff] [blame] | 846 | return read == limit ? buffer : Arrays.copyOf(buffer, read); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 847 | } |
| 848 | } |
| 849 | |
| 850 | /** |
Nathan Harmata | 4e69824 | 2015-10-20 23:18:23 +0000 | [diff] [blame] | 851 | * Reads the given file {@code path}, assumed to have size {@code fileSize}, and does a sanity |
| 852 | * check on the number of bytes read. |
| 853 | * |
| 854 | * <p>Use this method when you already know the size of the file. The sanity check is intended to |
| 855 | * catch issues where filesystems incorrectly truncate files. |
| 856 | * |
| 857 | * @throws IOException if there was an error, or if fewer than {@code fileSize} bytes were read. |
| 858 | */ |
| 859 | public static byte[] readWithKnownFileSize(Path path, long fileSize) throws IOException { |
| 860 | if (fileSize > Integer.MAX_VALUE) { |
| 861 | throw new IOException("Cannot read file with size larger than 2GB"); |
| 862 | } |
| 863 | int fileSizeInt = (int) fileSize; |
| 864 | byte[] bytes = readContentWithLimit(path, fileSizeInt); |
| 865 | if (fileSizeInt > bytes.length) { |
| 866 | throw new IOException("Unexpected short read from file '" + path |
| 867 | + "' (expected " + fileSizeInt + ", got " + bytes.length + " bytes)"); |
| 868 | } |
| 869 | return bytes; |
| 870 | } |
| 871 | |
| 872 | /** |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 873 | * Dumps diagnostic information about the specified filesystem to {@code out}. |
| 874 | * This is the implementation of the filesystem part of the 'blaze dump' |
| 875 | * command. It lives here, rather than in DumpCommand, because it requires |
| 876 | * privileged access to members of this package. |
| 877 | * |
| 878 | * <p>Its results are unspecified and MUST NOT be interpreted programmatically. |
| 879 | */ |
| 880 | public static void dump(FileSystem fs, final PrintStream out) { |
| 881 | if (!(fs instanceof UnixFileSystem)) { |
| 882 | out.println(" Not a UnixFileSystem."); |
| 883 | return; |
| 884 | } |
| 885 | |
| 886 | // Unfortunately there's no "letrec" for anonymous functions so we have to |
| 887 | // (a) name the function, (b) put it in a box and (c) use List not array |
| 888 | // because of the generic type. *sigh*. |
| 889 | final List<Predicate<Path>> dumpFunction = new ArrayList<>(); |
| 890 | dumpFunction.add(new Predicate<Path>() { |
| 891 | @Override |
| 892 | public boolean apply(Path child) { |
| 893 | Path path = child; |
| 894 | out.println(" " + path + " (" + path.toDebugString() + ")"); |
| 895 | path.applyToChildren(dumpFunction.get(0)); |
| 896 | return false; |
| 897 | } |
| 898 | }); |
| 899 | |
| 900 | fs.getRootDirectory().applyToChildren(dumpFunction.get(0)); |
| 901 | } |
| 902 | |
| 903 | /** |
| 904 | * Returns the type of the file system path belongs to. |
| 905 | */ |
| 906 | public static String getFileSystem(Path path) { |
| 907 | return path.getFileSystem().getFileSystemType(path); |
| 908 | } |
| 909 | |
| 910 | /** |
| 911 | * Returns whether the given path starts with any of the paths in the given |
| 912 | * list of prefixes. |
| 913 | */ |
| 914 | public static boolean startsWithAny(Path path, Iterable<Path> prefixes) { |
| 915 | for (Path prefix : prefixes) { |
| 916 | if (path.startsWith(prefix)) { |
| 917 | return true; |
| 918 | } |
| 919 | } |
| 920 | return false; |
| 921 | } |
| 922 | |
| 923 | /** |
| 924 | * Returns whether the given path starts with any of the paths in the given |
| 925 | * list of prefixes. |
| 926 | */ |
| 927 | public static boolean startsWithAny(PathFragment path, Iterable<PathFragment> prefixes) { |
| 928 | for (PathFragment prefix : prefixes) { |
| 929 | if (path.startsWith(prefix)) { |
| 930 | return true; |
| 931 | } |
| 932 | } |
| 933 | return false; |
| 934 | } |
Googler | e1cd950 | 2016-09-07 14:33:29 +0000 | [diff] [blame^] | 935 | |
| 936 | |
| 937 | /** |
| 938 | * Create a new hard link file at "linkPath" for file at "originalPath". If "originalPath" is a |
| 939 | * directory, then for each entry, create link under "linkPath" recursively. |
| 940 | * |
| 941 | * @param linkPath The path of the new link file to be created |
| 942 | * @param originalPath The path of the original file |
| 943 | * @throws IOException if there was an error executing {@link Path#createHardLink} |
| 944 | */ |
| 945 | public static void createHardLink(Path linkPath, Path originalPath) throws IOException { |
| 946 | |
| 947 | // Regular file |
| 948 | if (originalPath.isFile()) { |
| 949 | Path parentDir = linkPath.getParentDirectory(); |
| 950 | if (!parentDir.exists()) { |
| 951 | FileSystemUtils.createDirectoryAndParents(parentDir); |
| 952 | } |
| 953 | originalPath.createHardLink(linkPath); |
| 954 | // Directory |
| 955 | } else if (originalPath.isDirectory()) { |
| 956 | for (Path originalSubpath : originalPath.getDirectoryEntries()) { |
| 957 | Path linkSubpath = linkPath.getRelative(originalSubpath.relativeTo(originalPath)); |
| 958 | createHardLink(linkSubpath, originalSubpath); |
| 959 | } |
| 960 | } |
| 961 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 962 | } |