mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-04 15:18:09 +00:00
Changes to filesystem API and nio implementation
* Partial implementation of nio filesystem * Removed timeouts from openReadable and openWritable * Added convenience methods for copying * Added utility to support deadlock safe opening of multiple files
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
class Copier {
|
||||
|
||||
public static void copy(Folder source, Folder destination) {
|
||||
assertFoldersAreNotNested(source, destination);
|
||||
|
||||
destination.delete();
|
||||
destination.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
|
||||
source.files().forEach(sourceFile -> {
|
||||
File destinationFile = destination.file(sourceFile.name());
|
||||
copy(sourceFile, destinationFile);
|
||||
});
|
||||
|
||||
source.folders().forEach(sourceFolder -> {
|
||||
Folder destinationFolder = destination.folder(sourceFolder.name());
|
||||
sourceFolder.copyTo(destinationFolder);
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertFoldersAreNotNested(Folder source, Folder destination) {
|
||||
if (source.isAncestorOf(destination)) {
|
||||
throw new IllegalArgumentException("Can not copy parent to child directory (src: " + source + ", dst: " + destination + ")");
|
||||
}
|
||||
if (destination.isAncestorOf(source)) {
|
||||
throw new IllegalArgumentException("Can not copy child to parent directory (src: " + source + ", dst: " + destination + ")");
|
||||
}
|
||||
}
|
||||
|
||||
public static void copy(File source, File destination) {
|
||||
try (OpenFiles openFiles = DeadlockSafeFileOpener.withReadable(source).andWritable(destination).open()) {
|
||||
openFiles.readable(source).copyTo(openFiles.writable(destination));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DeadlockSafeFileOpener {
|
||||
|
||||
public static DeadlockSafeFileOpener withReadable(File file) {
|
||||
return new DeadlockSafeFileOpener().andReadable(file);
|
||||
}
|
||||
|
||||
public static DeadlockSafeFileOpener withWritable(File file) {
|
||||
return new DeadlockSafeFileOpener().andWritable(file);
|
||||
}
|
||||
|
||||
private final SortedMap<File, Consumer<File>> filesWithOperation = new TreeMap<>();
|
||||
|
||||
private final Map<File, ReadableFile> readableFiles = new HashMap<>();
|
||||
private final Map<File, WritableFile> writableFiles = new HashMap<>();
|
||||
|
||||
private DeadlockSafeFileOpener() {
|
||||
}
|
||||
|
||||
public DeadlockSafeFileOpener andReadable(File file) {
|
||||
if (filesWithOperation.put(file, this::openReadable) != null) {
|
||||
throw new IllegalArgumentException(format("File %s already marked for opening", file));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public DeadlockSafeFileOpener andWritable(File file) {
|
||||
if (filesWithOperation.put(file, this::openWritable) != null) {
|
||||
throw new IllegalArgumentException(format("File %s already marked for opening", file));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void openReadable(File file) {
|
||||
readableFiles.put(file, file.openReadable());
|
||||
}
|
||||
|
||||
private void openWritable(File file) {
|
||||
writableFiles.put(file, file.openWritable());
|
||||
}
|
||||
|
||||
public OpenFiles open() {
|
||||
try {
|
||||
filesWithOperation.forEach((file, openAction) -> openAction.accept(file));
|
||||
} catch (RuntimeException e) {
|
||||
OpenFiles.cleanup(readableFiles.values(), writableFiles.values());
|
||||
throw e;
|
||||
}
|
||||
return new OpenFiles(readableFiles, writableFiles);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,15 +7,13 @@ package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* A {@link File} in a {@link FileSystem}.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public interface File extends Node {
|
||||
public interface File extends Node, Comparable<File> {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -34,17 +32,13 @@ public interface File extends Node {
|
||||
* In addition implementations may block to lock the required IO resources
|
||||
* to read the file.
|
||||
*
|
||||
* @param timeout
|
||||
* the timeout to wait until failing with a
|
||||
* {@link TimeoutException}
|
||||
* @param unit
|
||||
* the {@link TimeUnit} of the timeout value
|
||||
* @return a {@link ReadableFile} to work with
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while opening the file, the
|
||||
* file does not exist or is a directory
|
||||
*/
|
||||
ReadableFile openReadable(long timeout, TimeUnit unit) throws UncheckedIOException, TimeoutException;
|
||||
|
||||
ReadableFile openReadable() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -54,8 +48,9 @@ public interface File extends Node {
|
||||
* <p>
|
||||
* An implementation guarantees, that per {@link FileSystem} and
|
||||
* {@code File} only one {@link WritableFile} is open at a time. A
|
||||
* {@link WritableFile} is open when returned from this method and not yet
|
||||
* closed using {@link WritableFile#close()}.<br>
|
||||
* {@code WritableFile} is open when returned from this method and not yet
|
||||
* closed using {@link WritableFile#close()} or
|
||||
* {@link WritableFile#delete()}.<br>
|
||||
* In addition while a {@code WritableFile} is open no {@link ReadableFile}
|
||||
* can be open and vice versa.
|
||||
* <p>
|
||||
@@ -65,16 +60,15 @@ public interface File extends Node {
|
||||
* In addition implementations may block to lock the required IO resources
|
||||
* to read the file.
|
||||
*
|
||||
* @param timeout
|
||||
* the timeout to wait until failing with a
|
||||
* {@link TimeoutException}
|
||||
* @param unit
|
||||
* the {@link TimeUnit} of the timeout value
|
||||
* @return a {@link WritableFile} to work with
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while opening the file or
|
||||
* the file is a directory
|
||||
*/
|
||||
WritableFile openWritable(long timeout, TimeUnit unit) throws UncheckedIOException, TimeoutException;
|
||||
WritableFile openWritable() throws UncheckedIOException;
|
||||
|
||||
default void copyTo(File destination) {
|
||||
Copier.copy(this, destination);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ package org.cryptomator.filesystem;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -58,59 +56,53 @@ public interface Folder extends Node {
|
||||
Folder folder(String name) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Creates the directory, if it doesn't exist yet. No effect, if folder already exists. After successful invocation {@link #exists()} will return <code>true</code>.
|
||||
* Creates the directory, if it doesn't exist yet. No effect, if folder
|
||||
* already exists.
|
||||
*
|
||||
* @param mode Depending on this option either the attempt is made to recursively create all parent directories or an exception is thrown if the parent doesn't exist yet.
|
||||
* @throws UncheckedIOException wrapping an {@link FileNotFoundException}, if mode is {@link FolderCreateMode#FAIL_IF_PARENT_IS_MISSING FAIL_IF_PARENT_IS_MISSING} and parent doesn't exist.
|
||||
* @param mode
|
||||
* Depending on this option either the attempt is made to
|
||||
* recursively create all parent directories or an exception is
|
||||
* thrown if the parent doesn't exist yet.
|
||||
* @throws UncheckedIOException
|
||||
* wrapping an {@link FileNotFoundException}, if mode is
|
||||
* {@link FolderCreateMode#FAIL_IF_PARENT_IS_MISSING
|
||||
* FAIL_IF_PARENT_IS_MISSING} and parent doesn't exist.
|
||||
*/
|
||||
void create(FolderCreateMode mode) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Recusively copies this directory and all its contents to (not into) the given destination, creating nonexisting parent directories.
|
||||
* If the target exists it is deleted before performing the copy.
|
||||
* Recusively copies this directory and all its contents to (not into) the
|
||||
* given destination, creating nonexisting parent directories. If the target
|
||||
* exists it is deleted before performing the copy.
|
||||
*
|
||||
* @param target Destination folder. Must not be a descendant of this folder.
|
||||
* @param target
|
||||
* Destination folder. Must not be a descendant of this folder.
|
||||
*/
|
||||
default void copyTo(Folder target) throws UncheckedIOException {
|
||||
if (this.isAncestorOf(target)) {
|
||||
throw new IllegalArgumentException("Can not copy parent to child directory (src: " + this + ", dst: " + target + ")");
|
||||
}
|
||||
|
||||
// remove previous contents:
|
||||
if (target.exists()) {
|
||||
target.delete();
|
||||
}
|
||||
|
||||
// make sure target directory exists:
|
||||
target.create(FolderCreateMode.INCLUDING_PARENTS);
|
||||
assert target.exists();
|
||||
|
||||
// copy files:
|
||||
files().forEach(srcFile -> {
|
||||
try (ReadableFile src = srcFile.openReadable(1, TimeUnit.SECONDS)) {
|
||||
final File dstFile = target.file(srcFile.name());
|
||||
try (WritableFile dst = dstFile.openWritable(1, TimeUnit.MILLISECONDS)) {
|
||||
src.copyTo(dst);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Destination file (" + dstFile + ") must not exist yet, thus can't be locked.");
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
throw new UncheckedIOException(new IOException("Failed to lock source file (" + srcFile + ") in time.", e));
|
||||
}
|
||||
});
|
||||
|
||||
// copy subdirectories:
|
||||
folders().forEach(folder -> folder.copyTo(target.folder(folder.name())));
|
||||
Copier.copy(this, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directory including all child elements. Afterwards {@link #exists()} will return <code>false</code>.
|
||||
* <p>
|
||||
* Deletes the directory including all child elements.
|
||||
* <p>
|
||||
* If the directory does not exist this method does nothing.
|
||||
*/
|
||||
void delete() throws UncheckedIOException;
|
||||
default void delete() throws UncheckedIOException {
|
||||
if (!exists()) {
|
||||
return;
|
||||
}
|
||||
folders().forEach(Folder::delete);
|
||||
files().forEach(file -> {
|
||||
try (WritableFile writableFile = file.openWritable()) {
|
||||
writableFile.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves this directory and its contents to the given destination. If the target exists it is deleted before performing the move.
|
||||
* Afterwards {@link #exists()} will return <code>false</code> for this folder and any child nodes.
|
||||
* Moves this directory and its contents to the given destination. If the
|
||||
* target exists it is deleted before performing the move.
|
||||
*/
|
||||
void moveTo(Folder target);
|
||||
|
||||
@@ -135,9 +127,11 @@ public interface Folder extends Node {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively checks whether this folder or any subfolder contains the given node.
|
||||
* Recursively checks whether this folder or any subfolder contains the
|
||||
* given node.
|
||||
*
|
||||
* @param node Potential child, grandchild, ...
|
||||
* @param node
|
||||
* Potential child, grandchild, ...
|
||||
* @return <code>true</code> if this folder is an ancestor of the node.
|
||||
*/
|
||||
default boolean isAncestorOf(Node node) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenFiles implements AutoCloseable {
|
||||
|
||||
private final static Logger LOG = LoggerFactory.getLogger(OpenFiles.class);
|
||||
|
||||
private final Map<File, ReadableFile> readableFiles;
|
||||
private final Map<File, WritableFile> writableFiles;
|
||||
|
||||
public OpenFiles(Map<File, ReadableFile> readableFiles, Map<File, WritableFile> writableFiles) {
|
||||
this.readableFiles = readableFiles;
|
||||
this.writableFiles = writableFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
OpenFiles.cleanup(readableFiles.values(), writableFiles.values());
|
||||
}
|
||||
|
||||
public ReadableFile readable(File file) {
|
||||
return readableFiles.computeIfAbsent(file, fileNotOpenForReading -> {
|
||||
throw new IllegalArgumentException(String.format("File %s is not open for reading", fileNotOpenForReading));
|
||||
});
|
||||
}
|
||||
|
||||
public WritableFile writable(File file) {
|
||||
return writableFiles.computeIfAbsent(file, fileNotOpenForWriting -> {
|
||||
throw new IllegalArgumentException(String.format("File %s is not open for writing", fileNotOpenForWriting));
|
||||
});
|
||||
}
|
||||
|
||||
static void cleanup(Collection<ReadableFile> readableFiles, Collection<WritableFile> writableFiles) {
|
||||
Iterator<AutoCloseable> iterator = Stream.concat(readableFiles.stream(), writableFiles.stream()).iterator();
|
||||
UncheckedIOException firstException = null;
|
||||
while (iterator.hasNext()) {
|
||||
AutoCloseable openFile = iterator.next();
|
||||
try {
|
||||
openFile.close();
|
||||
} catch (UncheckedIOException e) {
|
||||
if (firstException == null) {
|
||||
firstException = e;
|
||||
} else {
|
||||
firstException.addSuppressed(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected exception during close on " + openFile.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
if (firstException != null) {
|
||||
throw firstException;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
public interface ReadableFile extends File, ReadableBytes, AutoCloseable {
|
||||
public interface ReadableFile extends ReadableBytes, AutoCloseable {
|
||||
|
||||
void copyTo(WritableFile other) throws UncheckedIOException;
|
||||
|
||||
|
||||
@@ -8,16 +8,33 @@ package org.cryptomator.filesystem;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
|
||||
public interface WritableFile extends File, WritableBytes, AutoCloseable {
|
||||
public interface WritableFile extends WritableBytes, AutoCloseable {
|
||||
|
||||
void moveTo(WritableFile other) throws UncheckedIOException;
|
||||
|
||||
void setLastModified(Instant instant) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Deletes this file from the file system.
|
||||
* <p>
|
||||
* Deleting a file causes it to be {@link WritableFile#close() closed}.
|
||||
*/
|
||||
void delete() throws UncheckedIOException;
|
||||
|
||||
void truncate() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Closes this {@code WritableFile} which finally commits all operations
|
||||
* performed on it to the underlying file system.
|
||||
* <p>
|
||||
* After a {@code WritableFile} has been closed all other operations will
|
||||
* throw an {@link UncheckedIOException}.
|
||||
* <p>
|
||||
* Invoking this method on a {@link WritableFile} which has already been
|
||||
* closed does nothing.
|
||||
*/
|
||||
@Override
|
||||
void close() throws UncheckedIOException;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user