diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java index e9c502d3f..82889bc1d 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/File.java @@ -71,4 +71,8 @@ public interface File extends Node, Comparable { Copier.copy(this, destination); } + default void moveTo(File destination) { + Mover.move(this, destination); + } + } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java new file mode 100644 index 000000000..957ac1a6c --- /dev/null +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Mover.java @@ -0,0 +1,11 @@ +package org.cryptomator.filesystem; + +class Mover { + + public static void move(File source, File destination) { + try (OpenFiles openFiles = DeadlockSafeFileOpener.withWritable(source).andWritable(destination).open()) { + openFiles.writable(source).moveTo(openFiles.writable(destination)); + } + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java index 53b4c7590..d002c0498 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java @@ -140,9 +140,8 @@ class CryptoFolder extends CryptoNode implements Folder { target.physicalFile().parent().get().create(FolderCreateMode.INCLUDING_PARENTS); assert target.physicalFile().parent().get().exists(); - try (WritableFile src = this.physicalFile().openWritable(); WritableFile dst = target.physicalFile().openWritable()) { - src.moveTo(dst); - } + this.physicalFile().moveTo(target.physicalFile()); + // directoryId is now used by target, we must no longer use the same id // (we'll generate a new one when needed) directoryId.set(null); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java index 475a7fdba..0fc4d37e8 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java @@ -71,7 +71,12 @@ class CryptoReadableFile implements ReadableFile { @Override public void copyTo(WritableFile other) { - file.copyTo(other); + if (other instanceof CryptoWritableFile) { + CryptoWritableFile dst = (CryptoWritableFile) other; + file.copyTo(dst.file); + } else { + throw new IllegalArgumentException("Can not move CryptoFile to conventional File."); + } } @Override diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java index 0b10792e7..0694749a1 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java @@ -17,14 +17,14 @@ import org.cryptomator.io.ByteBuffers; class CryptoWritableFile implements WritableFile { + final WritableFile file; private final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); private final FileContentEncryptor encryptor; - private final WritableFile file; private final Future writeTask; public CryptoWritableFile(FileContentCryptor cryptor, WritableFile file) { - this.encryptor = cryptor.createFileContentEncryptor(Optional.empty()); this.file = file; + this.encryptor = cryptor.createFileContentEncryptor(Optional.empty()); writeHeader(); this.writeTask = executorService.submit(new Writer()); } @@ -55,7 +55,12 @@ class CryptoWritableFile implements WritableFile { @Override public void moveTo(WritableFile other) { - file.moveTo(other); + if (other instanceof CryptoWritableFile) { + CryptoWritableFile dst = (CryptoWritableFile) other; + file.moveTo(dst.file); + } else { + throw new IllegalArgumentException("Can not move CryptoFile to conventional File."); + } } @Override diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java index 2b26245bd..b63fa8665 100644 --- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java @@ -13,6 +13,7 @@ import java.io.UncheckedIOException; import java.nio.file.FileAlreadyExistsException; import java.time.Instant; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import java.util.stream.Stream; @@ -101,15 +102,22 @@ class InMemoryFolder extends InMemoryNode implements Folder { @Override public void delete() { - // delete subfolder recursively: - folders().forEach(Folder::delete); - // delete direct children (this deletes files): - this.children.clear(); // remove ourself from parent: parent.children.computeIfPresent(name, (k, v) -> { // returning null removes the entry. return null; }); + // delete all children: + for (Iterator> iterator = children.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = iterator.next(); + iterator.remove(); + // recursively on folders: + if (entry.getValue() instanceof InMemoryFolder) { + InMemoryFolder subFolder = (InMemoryFolder) entry.getValue(); + // this will try to itself from our children, which is ok as we're using an iterator here. + subFolder.delete(); + } + } assert!this.exists(); } diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java index 785e8f1bf..d6d00bfcf 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/filters/UriNormalizationFilter.java @@ -20,6 +20,7 @@ public class UriNormalizationFilter implements HttpFilter { private static final String[] FILE_METHODS = {"PUT"}; private static final String[] DIRECTORY_METHODS = {"MKCOL"}; + private static final String MOVE = "MOVE"; @Override public void init(FilterConfig filterConfig) throws ServletException { @@ -32,6 +33,8 @@ public class UriNormalizationFilter implements HttpFilter { chain.doFilter(new FileUriRequest(request), response); } else if (ArrayUtils.contains(DIRECTORY_METHODS, request.getMethod().toUpperCase())) { chain.doFilter(new DirectoryUriRequest(request), response); + } else if (MOVE.equalsIgnoreCase(request.getMethod())) { + chain.doFilter(new CanonicalMoveRequest(request), response); } else { chain.doFilter(request, response); } @@ -42,6 +45,37 @@ public class UriNormalizationFilter implements HttpFilter { // no-op } + /** + * Makes the destination header end on "/" if moving a directory and remove additional "/" if moving a file. + */ + private static class CanonicalMoveRequest extends HttpServletRequestWrapper { + + private static String DESTINATION_HEADER = "Destination"; + + public CanonicalMoveRequest(HttpServletRequest request) { + super(request); + } + + @Override + public String getHeader(String name) { + if (name.equalsIgnoreCase(DESTINATION_HEADER)) { + return sameSuffixAsUri(super.getHeader(name)); + } else { + return super.getHeader(name); + } + } + + private String sameSuffixAsUri(String str) { + final String uri = this.getRequestURI(); + if (uri.endsWith("/")) { + return StringUtils.appendIfMissing(str, "/"); + } else { + return StringUtils.removeEnd(str, "/"); + } + } + + } + /** * HTTP request, whose URI never ends on "/". */ diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java index 12cbb1857..63c9e1243 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFile.java @@ -72,14 +72,22 @@ class DavFile extends DavNode { @Override public void move(DavResource destination) throws DavException { - // TODO Auto-generated method stub - + if (destination instanceof DavFile) { + DavFile dst = (DavFile) destination; + node.moveTo(dst.node); + } else { + throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); + } } @Override public void copy(DavResource destination, boolean shallow) throws DavException { - // TODO Auto-generated method stub - + if (destination instanceof DavFile) { + DavFile dst = (DavFile) destination; + node.copyTo(dst.node); + } else { + throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); + } } @Override diff --git a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java index 29c0d5f61..cc6af380e 100644 --- a/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java +++ b/main/jackrabbit-filesystem-adapter/src/main/java/org/cryptomator/webdav/jackrabbit/DavFolder.java @@ -22,6 +22,7 @@ import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceIterator; import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; @@ -32,6 +33,7 @@ import org.apache.jackrabbit.webdav.property.ResourceType; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; +import org.cryptomator.filesystem.Node; import org.cryptomator.filesystem.WritableFile; import org.cryptomator.webdav.jackrabbit.DavPathFactory.DavPath; @@ -83,37 +85,66 @@ class DavFolder extends DavNode { @Override public DavResourceIterator getMembers() { - final Stream folders = node.folders().map(this::getMemberFolder); - final Stream files = node.files().map(this::getMemberFile); + final Stream folders = node.folders().map(this::folderToDavFolder); + final Stream files = node.files().map(this::fileToDavFile); return new DavResourceIteratorImpl(Stream.concat(folders, files).collect(Collectors.toList())); } - private DavFolder getMemberFolder(Folder memberFolder) { + private DavFolder folderToDavFolder(Folder memberFolder) { final DavPath subFolderLocator = path.getChild(memberFolder.name() + '/'); return factory.createFolder(memberFolder, subFolderLocator, session); } - private DavFile getMemberFile(File memberFile) { + private DavFile fileToDavFile(File memberFile) { final DavPath subFolderLocator = path.getChild(memberFile.name()); return factory.createFile(memberFile, subFolderLocator, session); } @Override public void removeMember(DavResource member) throws DavException { - // TODO Auto-generated method stub + final Node child = getMemberNode(member.getDisplayName()); + if (child instanceof Folder) { + Folder folder = (Folder) child; + folder.delete(); + } else if (child instanceof File) { + File file = (File) child; + try (WritableFile writable = file.openWritable()) { + writable.delete(); + } + } else { + throw new IllegalStateException("Unexpected node type: " + child.getClass().getName()); + } + } + /** + * @throws DavException Error 404 if no child with the given name exists + */ + private Node getMemberNode(String name) throws DavException { + return node.children().filter(c -> c.name().equals(name)).findAny().orElseThrow(() -> { + return new DavException(DavServletResponse.SC_NOT_FOUND, "No such file or directory: " + path + name); + }); } @Override public void move(DavResource destination) throws DavException { - // TODO Auto-generated method stub - + if (destination instanceof DavFolder) { + DavFolder dst = (DavFolder) destination; + node.moveTo(dst.node); + } else { + throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); + } } @Override public void copy(DavResource destination, boolean shallow) throws DavException { - // TODO Auto-generated method stub - + if (shallow) { + throw new UnsupportedOperationException("Shallow copy of directories not supported."); + } else if (destination instanceof DavFolder) { + DavFolder dst = (DavFolder) destination; + node.copyTo(dst.node); + } else { + throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); + } } @Override