• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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