1 /* 2 * Copyright 2013 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.jimfs; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; 21 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 22 import static java.nio.file.StandardOpenOption.CREATE; 23 import static java.nio.file.StandardOpenOption.CREATE_NEW; 24 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 25 import static java.nio.file.StandardOpenOption.WRITE; 26 27 import com.google.common.base.Supplier; 28 import com.google.common.collect.ImmutableMap; 29 import com.google.common.collect.ImmutableSortedSet; 30 import com.google.common.collect.Lists; 31 import java.io.IOException; 32 import java.nio.file.CopyOption; 33 import java.nio.file.DirectoryNotEmptyException; 34 import java.nio.file.DirectoryStream; 35 import java.nio.file.FileAlreadyExistsException; 36 import java.nio.file.FileSystemException; 37 import java.nio.file.LinkOption; 38 import java.nio.file.NoSuchFileException; 39 import java.nio.file.OpenOption; 40 import java.nio.file.Path; 41 import java.nio.file.SecureDirectoryStream; 42 import java.nio.file.attribute.BasicFileAttributes; 43 import java.nio.file.attribute.FileAttribute; 44 import java.nio.file.attribute.FileAttributeView; 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Objects; 48 import java.util.Set; 49 import java.util.concurrent.locks.Lock; 50 import java.util.concurrent.locks.ReadWriteLock; 51 import org.checkerframework.checker.nullness.compatqual.NullableDecl; 52 53 /** 54 * View of a file system with a specific working directory. As all file system operations need to 55 * work when given either relative or absolute paths, this class contains the implementation of most 56 * file system operations, with relative path operations resolving against the working directory. 57 * 58 * <p>A file system has one default view using the file system's working directory. Additional views 59 * may be created for use in {@link SecureDirectoryStream} instances, which each have a different 60 * working directory they use. 61 * 62 * @author Colin Decker 63 */ 64 final class FileSystemView { 65 66 private final JimfsFileStore store; 67 68 private final Directory workingDirectory; 69 private final JimfsPath workingDirectoryPath; 70 71 /** Creates a new file system view. */ FileSystemView( JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath)72 public FileSystemView( 73 JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath) { 74 this.store = checkNotNull(store); 75 this.workingDirectory = checkNotNull(workingDirectory); 76 this.workingDirectoryPath = checkNotNull(workingDirectoryPath); 77 } 78 79 /** Returns whether or not this view and the given view belong to the same file system. */ isSameFileSystem(FileSystemView other)80 private boolean isSameFileSystem(FileSystemView other) { 81 return store == other.store; 82 } 83 84 /** Returns the file system state. */ state()85 public FileSystemState state() { 86 return store.state(); 87 } 88 89 /** 90 * Returns the path of the working directory at the time this view was created. Does not reflect 91 * changes to the path caused by the directory being moved. 92 */ getWorkingDirectoryPath()93 public JimfsPath getWorkingDirectoryPath() { 94 return workingDirectoryPath; 95 } 96 97 /** Attempt to look up the file at the given path. */ lookUpWithLock(JimfsPath path, Set<? super LinkOption> options)98 DirectoryEntry lookUpWithLock(JimfsPath path, Set<? super LinkOption> options) 99 throws IOException { 100 store.readLock().lock(); 101 try { 102 return lookUp(path, options); 103 } finally { 104 store.readLock().unlock(); 105 } 106 } 107 108 /** Looks up the file at the given path without locking. */ lookUp(JimfsPath path, Set<? super LinkOption> options)109 private DirectoryEntry lookUp(JimfsPath path, Set<? super LinkOption> options) 110 throws IOException { 111 return store.lookUp(workingDirectory, path, options); 112 } 113 114 /** 115 * Creates a new directory stream for the directory located by the given path. The given {@code 116 * basePathForStream} is that base path that the returned stream will use. This will be the same 117 * as {@code dir} except for streams created relative to another secure stream. 118 */ newDirectoryStream( JimfsPath dir, DirectoryStream.Filter<? super Path> filter, Set<? super LinkOption> options, JimfsPath basePathForStream)119 public DirectoryStream<Path> newDirectoryStream( 120 JimfsPath dir, 121 DirectoryStream.Filter<? super Path> filter, 122 Set<? super LinkOption> options, 123 JimfsPath basePathForStream) 124 throws IOException { 125 Directory file = (Directory) lookUpWithLock(dir, options).requireDirectory(dir).file(); 126 FileSystemView view = new FileSystemView(store, file, basePathForStream); 127 JimfsSecureDirectoryStream stream = new JimfsSecureDirectoryStream(view, filter, state()); 128 return store.supportsFeature(Feature.SECURE_DIRECTORY_STREAM) 129 ? stream 130 : new DowngradedDirectoryStream(stream); 131 } 132 133 /** Snapshots the entries of the working directory of this view. */ snapshotWorkingDirectoryEntries()134 public ImmutableSortedSet<Name> snapshotWorkingDirectoryEntries() { 135 store.readLock().lock(); 136 try { 137 ImmutableSortedSet<Name> names = workingDirectory.snapshot(); 138 workingDirectory.updateAccessTime(); 139 return names; 140 } finally { 141 store.readLock().unlock(); 142 } 143 } 144 145 /** 146 * Returns a snapshot mapping the names of each file in the directory at the given path to the 147 * last modified time of that file. 148 */ snapshotModifiedTimes(JimfsPath path)149 public ImmutableMap<Name, Long> snapshotModifiedTimes(JimfsPath path) throws IOException { 150 ImmutableMap.Builder<Name, Long> modifiedTimes = ImmutableMap.builder(); 151 152 store.readLock().lock(); 153 try { 154 Directory dir = (Directory) lookUp(path, Options.FOLLOW_LINKS).requireDirectory(path).file(); 155 // TODO(cgdecker): Investigate whether WatchServices should keep a reference to the actual 156 // directory when SecureDirectoryStream is supported rather than looking up the directory 157 // each time the WatchService polls 158 159 for (DirectoryEntry entry : dir) { 160 if (!entry.name().equals(Name.SELF) && !entry.name().equals(Name.PARENT)) { 161 modifiedTimes.put(entry.name(), entry.file().getLastModifiedTime()); 162 } 163 } 164 165 return modifiedTimes.build(); 166 } finally { 167 store.readLock().unlock(); 168 } 169 } 170 171 /** 172 * Returns whether or not the two given paths locate the same file. The second path is located 173 * using the given view rather than this file view. 174 */ isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2)175 public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2) 176 throws IOException { 177 if (!isSameFileSystem(view2)) { 178 return false; 179 } 180 181 store.readLock().lock(); 182 try { 183 File file = lookUp(path, Options.FOLLOW_LINKS).fileOrNull(); 184 File file2 = view2.lookUp(path2, Options.FOLLOW_LINKS).fileOrNull(); 185 return file != null && Objects.equals(file, file2); 186 } finally { 187 store.readLock().unlock(); 188 } 189 } 190 191 /** 192 * Gets the {@linkplain Path#toRealPath(LinkOption...) real path} to the file located by the given 193 * path. 194 */ toRealPath( JimfsPath path, PathService pathService, Set<? super LinkOption> options)195 public JimfsPath toRealPath( 196 JimfsPath path, PathService pathService, Set<? super LinkOption> options) throws IOException { 197 checkNotNull(path); 198 checkNotNull(options); 199 200 store.readLock().lock(); 201 try { 202 DirectoryEntry entry = lookUp(path, options).requireExists(path); 203 204 List<Name> names = new ArrayList<>(); 205 names.add(entry.name()); 206 while (!entry.file().isRootDirectory()) { 207 entry = entry.directory().entryInParent(); 208 names.add(entry.name()); 209 } 210 211 // names are ordered last to first in the list, so get the reverse view 212 List<Name> reversed = Lists.reverse(names); 213 Name root = reversed.remove(0); 214 return pathService.createPath(root, reversed); 215 } finally { 216 store.readLock().unlock(); 217 } 218 } 219 220 /** 221 * Creates a new directory at the given path. The given attributes will be set on the new file if 222 * possible. 223 */ createDirectory(JimfsPath path, FileAttribute<?>... attrs)224 public Directory createDirectory(JimfsPath path, FileAttribute<?>... attrs) throws IOException { 225 return (Directory) createFile(path, store.directoryCreator(), true, attrs); 226 } 227 228 /** 229 * Creates a new symbolic link at the given path with the given target. The given attributes will 230 * be set on the new file if possible. 231 */ createSymbolicLink( JimfsPath path, JimfsPath target, FileAttribute<?>... attrs)232 public SymbolicLink createSymbolicLink( 233 JimfsPath path, JimfsPath target, FileAttribute<?>... attrs) throws IOException { 234 if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) { 235 throw new UnsupportedOperationException(); 236 } 237 return (SymbolicLink) createFile(path, store.symbolicLinkCreator(target), true, attrs); 238 } 239 240 /** 241 * Creates a new file at the given path if possible, using the given supplier to create the file. 242 * Returns the new file. If {@code allowExisting} is {@code true} and a file already exists at the 243 * given path, returns that file. Otherwise, throws {@link FileAlreadyExistsException}. 244 */ createFile( JimfsPath path, Supplier<? extends File> fileCreator, boolean failIfExists, FileAttribute<?>... attrs)245 private File createFile( 246 JimfsPath path, 247 Supplier<? extends File> fileCreator, 248 boolean failIfExists, 249 FileAttribute<?>... attrs) 250 throws IOException { 251 checkNotNull(path); 252 checkNotNull(fileCreator); 253 254 store.writeLock().lock(); 255 try { 256 DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS); 257 258 if (entry.exists()) { 259 if (failIfExists) { 260 throw new FileAlreadyExistsException(path.toString()); 261 } 262 263 // currently can only happen if getOrCreateFile doesn't find the file with the read lock 264 // and then the file is created between when it releases the read lock and when it 265 // acquires the write lock; so, very unlikely 266 return entry.file(); 267 } 268 269 Directory parent = entry.directory(); 270 271 File newFile = fileCreator.get(); 272 store.setInitialAttributes(newFile, attrs); 273 parent.link(path.name(), newFile); 274 parent.updateModifiedTime(); 275 return newFile; 276 } finally { 277 store.writeLock().unlock(); 278 } 279 } 280 281 /** 282 * Gets the regular file at the given path, creating it if it doesn't exist and the given options 283 * specify that it should be created. 284 */ getOrCreateRegularFile( JimfsPath path, Set<OpenOption> options, FileAttribute<?>... attrs)285 public RegularFile getOrCreateRegularFile( 286 JimfsPath path, Set<OpenOption> options, FileAttribute<?>... attrs) throws IOException { 287 checkNotNull(path); 288 289 if (!options.contains(CREATE_NEW)) { 290 // assume file exists unless we're explicitly trying to create a new file 291 RegularFile file = lookUpRegularFile(path, options); 292 if (file != null) { 293 return file; 294 } 295 } 296 297 if (options.contains(CREATE) || options.contains(CREATE_NEW)) { 298 return getOrCreateRegularFileWithWriteLock(path, options, attrs); 299 } else { 300 throw new NoSuchFileException(path.toString()); 301 } 302 } 303 304 /** 305 * Looks up the regular file at the given path, throwing an exception if the file isn't a regular 306 * file. Returns null if the file did not exist. 307 */ 308 @NullableDecl lookUpRegularFile(JimfsPath path, Set<OpenOption> options)309 private RegularFile lookUpRegularFile(JimfsPath path, Set<OpenOption> options) 310 throws IOException { 311 store.readLock().lock(); 312 try { 313 DirectoryEntry entry = lookUp(path, options); 314 if (entry.exists()) { 315 File file = entry.file(); 316 if (!file.isRegularFile()) { 317 throw new FileSystemException(path.toString(), null, "not a regular file"); 318 } 319 return open((RegularFile) file, options); 320 } else { 321 return null; 322 } 323 } finally { 324 store.readLock().unlock(); 325 } 326 } 327 328 /** Gets or creates a new regular file with a write lock (assuming the file does not exist). */ getOrCreateRegularFileWithWriteLock( JimfsPath path, Set<OpenOption> options, FileAttribute<?>[] attrs)329 private RegularFile getOrCreateRegularFileWithWriteLock( 330 JimfsPath path, Set<OpenOption> options, FileAttribute<?>[] attrs) throws IOException { 331 store.writeLock().lock(); 332 try { 333 File file = createFile(path, store.regularFileCreator(), options.contains(CREATE_NEW), attrs); 334 // the file already existed but was not a regular file 335 if (!file.isRegularFile()) { 336 throw new FileSystemException(path.toString(), null, "not a regular file"); 337 } 338 return open((RegularFile) file, options); 339 } finally { 340 store.writeLock().unlock(); 341 } 342 } 343 344 /** 345 * Opens the given regular file with the given options, truncating it if necessary and 346 * incrementing its open count. Returns the given file. 347 */ open(RegularFile file, Set<OpenOption> options)348 private static RegularFile open(RegularFile file, Set<OpenOption> options) { 349 if (options.contains(TRUNCATE_EXISTING) && options.contains(WRITE)) { 350 file.writeLock().lock(); 351 try { 352 file.truncate(0); 353 } finally { 354 file.writeLock().unlock(); 355 } 356 } 357 358 // must be opened while holding a file store lock to ensure no race between opening and 359 // deleting the file 360 file.opened(); 361 362 return file; 363 } 364 365 /** Returns the target of the symbolic link at the given path. */ readSymbolicLink(JimfsPath path)366 public JimfsPath readSymbolicLink(JimfsPath path) throws IOException { 367 if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) { 368 throw new UnsupportedOperationException(); 369 } 370 371 SymbolicLink symbolicLink = 372 (SymbolicLink) 373 lookUpWithLock(path, Options.NOFOLLOW_LINKS).requireSymbolicLink(path).file(); 374 375 return symbolicLink.target(); 376 } 377 378 /** 379 * Checks access to the file at the given path for the given modes. Since access controls are not 380 * implemented for this file system, this just checks that the file exists. 381 */ checkAccess(JimfsPath path)382 public void checkAccess(JimfsPath path) throws IOException { 383 // just check that the file exists 384 lookUpWithLock(path, Options.FOLLOW_LINKS).requireExists(path); 385 } 386 387 /** 388 * Creates a hard link at the given link path to the regular file at the given path. The existing 389 * file must exist and must be a regular file. The given file system view must belong to the same 390 * file system as this view. 391 */ link(JimfsPath link, FileSystemView existingView, JimfsPath existing)392 public void link(JimfsPath link, FileSystemView existingView, JimfsPath existing) 393 throws IOException { 394 checkNotNull(link); 395 checkNotNull(existingView); 396 checkNotNull(existing); 397 398 if (!store.supportsFeature(Feature.LINKS)) { 399 throw new UnsupportedOperationException(); 400 } 401 402 if (!isSameFileSystem(existingView)) { 403 throw new FileSystemException( 404 link.toString(), 405 existing.toString(), 406 "can't link: source and target are in different file system instances"); 407 } 408 409 Name linkName = link.name(); 410 411 // existingView is in the same file system, so just one lock is needed 412 store.writeLock().lock(); 413 try { 414 // we do want to follow links when finding the existing file 415 File existingFile = 416 existingView.lookUp(existing, Options.FOLLOW_LINKS).requireExists(existing).file(); 417 if (!existingFile.isRegularFile()) { 418 throw new FileSystemException( 419 link.toString(), existing.toString(), "can't link: not a regular file"); 420 } 421 422 Directory linkParent = 423 lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory(); 424 425 linkParent.link(linkName, existingFile); 426 linkParent.updateModifiedTime(); 427 } finally { 428 store.writeLock().unlock(); 429 } 430 } 431 432 /** Deletes the file at the given absolute path. */ deleteFile(JimfsPath path, DeleteMode deleteMode)433 public void deleteFile(JimfsPath path, DeleteMode deleteMode) throws IOException { 434 store.writeLock().lock(); 435 try { 436 DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS).requireExists(path); 437 delete(entry, deleteMode, path); 438 } finally { 439 store.writeLock().unlock(); 440 } 441 } 442 443 /** Deletes the given directory entry from its parent directory. */ delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException)444 private void delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException) 445 throws IOException { 446 Directory parent = entry.directory(); 447 File file = entry.file(); 448 449 checkDeletable(file, deleteMode, pathForException); 450 parent.unlink(entry.name()); 451 parent.updateModifiedTime(); 452 453 file.deleted(); 454 } 455 456 /** Mode for deleting. Determines what types of files can be deleted. */ 457 public enum DeleteMode { 458 /** Delete any file. */ 459 ANY, 460 /** Only delete non-directory files. */ 461 NON_DIRECTORY_ONLY, 462 /** Only delete directory files. */ 463 DIRECTORY_ONLY 464 } 465 466 /** Checks that the given file can be deleted, throwing an exception if it can't. */ checkDeletable(File file, DeleteMode mode, Path path)467 private void checkDeletable(File file, DeleteMode mode, Path path) throws IOException { 468 if (file.isRootDirectory()) { 469 throw new FileSystemException(path.toString(), null, "can't delete root directory"); 470 } 471 472 if (file.isDirectory()) { 473 if (mode == DeleteMode.NON_DIRECTORY_ONLY) { 474 throw new FileSystemException(path.toString(), null, "can't delete: is a directory"); 475 } 476 477 checkEmpty(((Directory) file), path); 478 } else if (mode == DeleteMode.DIRECTORY_ONLY) { 479 throw new FileSystemException(path.toString(), null, "can't delete: is not a directory"); 480 } 481 482 if (file == workingDirectory && !path.isAbsolute()) { 483 // this is weird, but on Unix at least, the file system seems to be happy to delete the 484 // working directory if you give the absolute path to it but fail if you use a relative path 485 // that resolves to the working directory (e.g. "" or ".") 486 throw new FileSystemException(path.toString(), null, "invalid argument"); 487 } 488 } 489 490 /** Checks that given directory is empty, throwing {@link DirectoryNotEmptyException} if not. */ checkEmpty(Directory dir, Path pathForException)491 private void checkEmpty(Directory dir, Path pathForException) throws FileSystemException { 492 if (!dir.isEmpty()) { 493 throw new DirectoryNotEmptyException(pathForException.toString()); 494 } 495 } 496 497 /** Copies or moves the file at the given source path to the given dest path. */ copy( JimfsPath source, FileSystemView destView, JimfsPath dest, Set<CopyOption> options, boolean move)498 public void copy( 499 JimfsPath source, 500 FileSystemView destView, 501 JimfsPath dest, 502 Set<CopyOption> options, 503 boolean move) 504 throws IOException { 505 checkNotNull(source); 506 checkNotNull(destView); 507 checkNotNull(dest); 508 checkNotNull(options); 509 510 boolean sameFileSystem = isSameFileSystem(destView); 511 512 File sourceFile; 513 File copyFile = null; // non-null after block completes iff source file was copied 514 lockBoth(store.writeLock(), destView.store.writeLock()); 515 try { 516 DirectoryEntry sourceEntry = lookUp(source, options).requireExists(source); 517 DirectoryEntry destEntry = destView.lookUp(dest, Options.NOFOLLOW_LINKS); 518 519 Directory sourceParent = sourceEntry.directory(); 520 sourceFile = sourceEntry.file(); 521 522 Directory destParent = destEntry.directory(); 523 524 if (move && sourceFile.isDirectory()) { 525 if (sameFileSystem) { 526 checkMovable(sourceFile, source); 527 checkNotAncestor(sourceFile, destParent, destView); 528 } else { 529 // move to another file system is accomplished by copy-then-delete, so the source file 530 // must be deletable to be moved 531 checkDeletable(sourceFile, DeleteMode.ANY, source); 532 } 533 } 534 535 if (destEntry.exists()) { 536 if (destEntry.file().equals(sourceFile)) { 537 return; 538 } else if (options.contains(REPLACE_EXISTING)) { 539 destView.delete(destEntry, DeleteMode.ANY, dest); 540 } else { 541 throw new FileAlreadyExistsException(dest.toString()); 542 } 543 } 544 545 if (move && sameFileSystem) { 546 // Real move on the same file system. 547 sourceParent.unlink(source.name()); 548 sourceParent.updateModifiedTime(); 549 550 destParent.link(dest.name(), sourceFile); 551 destParent.updateModifiedTime(); 552 } else { 553 // Doing a copy OR a move to a different file system, which must be implemented by copy and 554 // delete. 555 556 // By default, don't copy attributes. 557 AttributeCopyOption attributeCopyOption = AttributeCopyOption.NONE; 558 if (move) { 559 // Copy only the basic attributes of the file to the other file system, as it may not 560 // support all the attribute views that this file system does. This also matches the 561 // behavior of moving a file to a foreign file system with a different 562 // FileSystemProvider. 563 attributeCopyOption = AttributeCopyOption.BASIC; 564 } else if (options.contains(COPY_ATTRIBUTES)) { 565 // As with move, if we're copying the file to a different file system, only copy its 566 // basic attributes. 567 attributeCopyOption = 568 sameFileSystem ? AttributeCopyOption.ALL : AttributeCopyOption.BASIC; 569 } 570 571 // Copy the file, but don't copy its content while we're holding the file store locks. 572 copyFile = destView.store.copyWithoutContent(sourceFile, attributeCopyOption); 573 destParent.link(dest.name(), copyFile); 574 destParent.updateModifiedTime(); 575 576 // In order for the copy to be atomic (not strictly necessary, but seems preferable since 577 // we can) lock both source and copy files before leaving the file store locks. This 578 // ensures that users cannot observe the copy's content until the content has been copied. 579 // This also marks the source file as opened, preventing its content from being deleted 580 // until after it's copied if the source file itself is deleted in the next step. 581 lockSourceAndCopy(sourceFile, copyFile); 582 583 if (move) { 584 // It should not be possible for delete to throw an exception here, because we already 585 // checked that the file was deletable above. 586 delete(sourceEntry, DeleteMode.ANY, source); 587 } 588 } 589 } finally { 590 destView.store.writeLock().unlock(); 591 store.writeLock().unlock(); 592 } 593 594 if (copyFile != null) { 595 // Copy the content. This is done outside the above block to minimize the time spent holding 596 // file store locks, since copying the content of a regular file could take a (relatively) 597 // long time. If done inside the above block, copying using Files.copy can be slower than 598 // copying with an InputStream and an OutputStream if many files are being copied on 599 // different threads. 600 try { 601 sourceFile.copyContentTo(copyFile); 602 } finally { 603 // Unlock the files, allowing the content of the copy to be observed by the user. This also 604 // closes the source file, allowing its content to be deleted if it was deleted. 605 unlockSourceAndCopy(sourceFile, copyFile); 606 } 607 } 608 } 609 checkMovable(File file, JimfsPath path)610 private void checkMovable(File file, JimfsPath path) throws FileSystemException { 611 if (file.isRootDirectory()) { 612 throw new FileSystemException(path.toString(), null, "can't move root directory"); 613 } 614 } 615 616 /** 617 * Acquires both write locks in a way that attempts to avoid the possibility of deadlock. Note 618 * that typically (when only one file system instance is involved), both locks will be the same 619 * lock and there will be no issue at all. 620 */ lockBoth(Lock sourceWriteLock, Lock destWriteLock)621 private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) { 622 while (true) { 623 sourceWriteLock.lock(); 624 if (destWriteLock.tryLock()) { 625 return; 626 } else { 627 sourceWriteLock.unlock(); 628 } 629 630 destWriteLock.lock(); 631 if (sourceWriteLock.tryLock()) { 632 return; 633 } else { 634 destWriteLock.unlock(); 635 } 636 } 637 } 638 639 /** Checks that source is not an ancestor of dest, throwing an exception if it is. */ checkNotAncestor(File source, Directory destParent, FileSystemView destView)640 private void checkNotAncestor(File source, Directory destParent, FileSystemView destView) 641 throws IOException { 642 // if dest is not in the same file system, it couldn't be in source's subdirectories 643 if (!isSameFileSystem(destView)) { 644 return; 645 } 646 647 Directory current = destParent; 648 while (true) { 649 if (current.equals(source)) { 650 throw new IOException( 651 "invalid argument: can't move directory into a subdirectory of itself"); 652 } 653 654 if (current.isRootDirectory()) { 655 return; 656 } else { 657 current = current.parent(); 658 } 659 } 660 } 661 662 /** 663 * Locks source and copy files before copying content. Also marks the source file as opened so 664 * that its content won't be deleted until after the copy if it is deleted. 665 */ lockSourceAndCopy(File sourceFile, File copyFile)666 private void lockSourceAndCopy(File sourceFile, File copyFile) { 667 sourceFile.opened(); 668 ReadWriteLock sourceLock = sourceFile.contentLock(); 669 if (sourceLock != null) { 670 sourceLock.readLock().lock(); 671 } 672 ReadWriteLock copyLock = copyFile.contentLock(); 673 if (copyLock != null) { 674 copyLock.writeLock().lock(); 675 } 676 } 677 678 /** 679 * Unlocks source and copy files after copying content. Also closes the source file so its content 680 * can be deleted if it was deleted. 681 */ unlockSourceAndCopy(File sourceFile, File copyFile)682 private void unlockSourceAndCopy(File sourceFile, File copyFile) { 683 ReadWriteLock sourceLock = sourceFile.contentLock(); 684 if (sourceLock != null) { 685 sourceLock.readLock().unlock(); 686 } 687 ReadWriteLock copyLock = copyFile.contentLock(); 688 if (copyLock != null) { 689 copyLock.writeLock().unlock(); 690 } 691 sourceFile.closed(); 692 } 693 694 /** Returns a file attribute view using the given lookup callback. */ 695 @NullableDecl getFileAttributeView(FileLookup lookup, Class<V> type)696 public <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) { 697 return store.getFileAttributeView(lookup, type); 698 } 699 700 /** Returns a file attribute view for the given path in this view. */ 701 @NullableDecl getFileAttributeView( final JimfsPath path, Class<V> type, final Set<? super LinkOption> options)702 public <V extends FileAttributeView> V getFileAttributeView( 703 final JimfsPath path, Class<V> type, final Set<? super LinkOption> options) { 704 return store.getFileAttributeView( 705 new FileLookup() { 706 @Override 707 public File lookup() throws IOException { 708 return lookUpWithLock(path, options).requireExists(path).file(); 709 } 710 }, 711 type); 712 } 713 714 /** Reads attributes of the file located by the given path in this view as an object. */ 715 public <A extends BasicFileAttributes> A readAttributes( 716 JimfsPath path, Class<A> type, Set<? super LinkOption> options) throws IOException { 717 File file = lookUpWithLock(path, options).requireExists(path).file(); 718 return store.readAttributes(file, type); 719 } 720 721 /** Reads attributes of the file located by the given path in this view as a map. */ 722 public ImmutableMap<String, Object> readAttributes( 723 JimfsPath path, String attributes, Set<? super LinkOption> options) throws IOException { 724 File file = lookUpWithLock(path, options).requireExists(path).file(); 725 return store.readAttributes(file, attributes); 726 } 727 728 /** 729 * Sets the given attribute to the given value on the file located by the given path in this view. 730 */ 731 public void setAttribute( 732 JimfsPath path, String attribute, Object value, Set<? super LinkOption> options) 733 throws IOException { 734 File file = lookUpWithLock(path, options).requireExists(path).file(); 735 store.setAttribute(file, attribute, value); 736 } 737 } 738