Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,9 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists
}

FileChannel ch = null;
try {
ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
FileChannel ch = null;
try {
ch = openCryptoFiles.getOrCreate(cleartextFilePath, ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
if (options.writable()) {
ciphertextPath.persistLongFileName();
stats.incrementAccessesWritten();
Expand Down Expand Up @@ -618,7 +618,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
// "the symbolic link itself, not the target of the link, is moved"
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), cleartextTarget)) {
Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
if (ciphertextTarget.isShortened()) {
ciphertextTarget.persistLongFileName();
Expand All @@ -634,7 +634,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
// we need to re-map the OpenCryptoFile entry.
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), cleartextTarget)) {
checkUsage(cleartextSource, ciphertextSource);
checkUsage(cleartextTarget, ciphertextTarget);
if (ciphertextTarget.isShortened()) {
Expand Down Expand Up @@ -742,4 +742,4 @@ void checkUsage(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) thr
}
}

}
}
4 changes: 2 additions & 2 deletions src/main/java/org/cryptomator/cryptofs/Symlinks.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag);
ByteBuffer content = UTF_8.encode(target.toString());
Files.createDirectory(ciphertextFilePath.getRawPath());
openCryptoFiles.writeCiphertextFile(ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
openCryptoFiles.writeCiphertextFile(cleartextPath, ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
ciphertextFilePath.persistLongFileName();
}

Expand All @@ -57,7 +57,7 @@ public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.READ), readonlyFlag);
assertIsSymlink(cleartextPath, ciphertextSymlinkFile);
try {
ByteBuffer content = openCryptoFiles.readCiphertextFile(ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
ByteBuffer content = openCryptoFiles.readCiphertextFile(cleartextPath, ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
return cleartextPath.getFileSystem().getPath(UTF_8.decode(content).toString());
} catch (BufferUnderflowException e) {
throw new NotLinkException(cleartextPath.toString(), null, "Unreasonably large symlink file");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import jakarta.inject.Inject;
import org.cryptomator.cryptofs.CryptoFileSystemStats;
import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.EffectiveOpenOptions;
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptofs.fh.BufferPool;
import org.cryptomator.cryptofs.fh.Chunk;
import org.cryptomator.cryptofs.fh.ChunkCache;
import org.cryptomator.cryptofs.fh.CurrentOpenFileCleartextPath;
import org.cryptomator.cryptofs.fh.CurrentOpenFilePath;
import org.cryptomator.cryptofs.fh.ExceptionsDuringWrite;
import org.cryptomator.cryptofs.fh.FileHeaderHolder;
import org.cryptomator.cryptofs.fh.OpenFileModifiedDate;
import org.cryptomator.cryptofs.fh.OpenFileSize;
import org.cryptomator.cryptofs.inuse.FileAlreadyInUseException;
import org.cryptomator.cryptofs.inuse.InUseManager;
import org.cryptomator.cryptofs.inuse.StubInUseManager;
import org.cryptomator.cryptofs.inuse.UseInfo;
import org.cryptomator.cryptolib.api.Cryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.inject.Inject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
Expand Down Expand Up @@ -55,10 +63,28 @@ public class CleartextFileChannel extends AbstractFileChannel {
private final AtomicReference<Instant> lastModified;
private final ExceptionsDuringWrite exceptionsDuringWrite;
private final Consumer<FileChannel> closeListener;
private final Consumer<FilesystemEvent> eventConsumer;
private final CryptoFileSystemStats stats;
private final InUseManager inUseManager;
private final AtomicReference<CryptoPath> currentCleartextPath;

@Inject
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, @CurrentOpenFilePath AtomicReference<Path> currentPath, ExceptionsDuringWrite exceptionsDuringWrite, Consumer<FileChannel> closeListener, CryptoFileSystemStats stats) {
public CleartextFileChannel(FileChannel ciphertextFileChannel, //
FileHeaderHolder fileHeaderHolder, //
ReadWriteLock readWriteLock, //
Cryptor cryptor, //
ChunkCache chunkCache, //
BufferPool bufferPool, //
EffectiveOpenOptions options, //
@OpenFileSize AtomicLong fileSize, //
@OpenFileModifiedDate AtomicReference<Instant> lastModified, //
@CurrentOpenFilePath AtomicReference<Path> currentPath, //
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
ExceptionsDuringWrite exceptionsDuringWrite, //
Consumer<FileChannel> closeListener, //
Consumer<FilesystemEvent> eventConsumer, //
CryptoFileSystemStats stats, //
InUseManager inUseManager) {
super(readWriteLock);
this.ciphertextFileChannel = ciphertextFileChannel;
this.fileHeaderHolder = fileHeaderHolder;
Expand All @@ -67,11 +93,14 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder
this.bufferPool = bufferPool;
this.options = options;
this.currentFilePath = currentPath;
this.currentCleartextPath = currentCleartextPath;
this.fileSize = fileSize;
this.lastModified = lastModified;
this.exceptionsDuringWrite = exceptionsDuringWrite;
this.closeListener = closeListener;
this.eventConsumer = eventConsumer;
this.stats = stats;
this.inUseManager = inUseManager;
if (options.append()) {
position = fileSize.get();
}
Expand All @@ -80,6 +109,23 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder
}
}

@VisibleForTesting
CleartextFileChannel(FileChannel ciphertextFileChannel, //
FileHeaderHolder fileHeaderHolder, //
ReadWriteLock readWriteLock, //
Cryptor cryptor, //
ChunkCache chunkCache, //
BufferPool bufferPool, //
EffectiveOpenOptions options, //
AtomicLong fileSize, //
AtomicReference<Instant> lastModified, //
AtomicReference<Path> currentPath, //
ExceptionsDuringWrite exceptionsDuringWrite, //
Consumer<FileChannel> closeListener, //
CryptoFileSystemStats stats) {
this(ciphertextFileChannel, fileHeaderHolder, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, currentPath, new AtomicReference<>(null), exceptionsDuringWrite, closeListener, ignored -> {}, stats, new StubInUseManager());
}

@Override
public long size() throws IOException {
assertOpen();
Expand Down Expand Up @@ -124,6 +170,17 @@ protected int readLocked(ByteBuffer dst, long position) throws IOException {

@Override
protected int writeLocked(ByteBuffer src, long position) throws IOException {
var path = currentFilePath.get();
if (path != null && inUseManager.isInUseByOthers(path)) {
var useInfo = inUseManager.getUseInfo(path).orElse(new UseInfo("UNKNOWN", Instant.now()));
var cleartextPath = currentCleartextPath.get();
if (cleartextPath != null) {
eventConsumer.accept(new FileIsInUseEvent(cleartextPath, path, useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(path)));
} else {
LOG.warn("Unable to emit FileIsInUseEvent: Cleartext path is null. Ciphertext path is {}.", path);
}
throw new FileAlreadyInUseException(path);
}
long oldFileSize = fileSize.get();
long written;
if (position > oldFileSize) {
Expand Down Expand Up @@ -256,8 +313,8 @@ void persistLastModified() throws IOException {
FileTime lastAccessTime = FileTime.from(Instant.now());
var p = currentFilePath.get();
if (p != null) {
p.getFileSystem().provider()//
.getFileAttributeView(p, BasicFileAttributeView.class)
p.getFileSystem().provider() //
.getFileAttributeView(p, BasicFileAttributeView.class) //
.setTimes(lastModifiedTime, lastAccessTime, null);
}

Expand Down Expand Up @@ -328,7 +385,7 @@ long beginOfChunk(long cleartextPos) {
protected void implCloseChannel() throws IOException {
var closeActions = List.<CloseAction>of(this::flush, //
super::implCloseChannel, //
() -> closeListener.accept(ciphertextFileChannel),
() -> closeListener.accept(ciphertextFileChannel), //
ciphertextFileChannel::close, //
this::tryPersistLastModified);
tryAll(closeActions.iterator());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.cryptofs.fh;

import jakarta.inject.Qualifier;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface CurrentOpenFileCleartextPath {
}
26 changes: 24 additions & 2 deletions src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cryptomator.cryptofs.fh;

import jakarta.inject.Inject;
import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.EffectiveOpenOptions;
import org.cryptomator.cryptofs.ch.CleartextFileChannel;
import org.cryptomator.cryptofs.inuse.InUseManager;
Expand Down Expand Up @@ -33,6 +34,7 @@ public class OpenCryptoFile implements Closeable {
private final FileHeaderHolder headerHolder;
private final ChunkIO chunkIO;
private final AtomicReference<Path> currentFilePath;
private final AtomicReference<CryptoPath> currentCleartextPath;
private final AtomicLong fileSize;
private final OpenCryptoFileComponent component;

Expand All @@ -42,22 +44,24 @@ public class OpenCryptoFile implements Closeable {
@Inject
public OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, //
@CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, //
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
@OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component, //
InUseManager inUseManager) {
this(listener, cryptor, headerHolder, chunkIO, currentFilePath, fileSize, lastModified, component, inUseManager, UseToken.CLOSED_TOKEN);
this(listener, cryptor, headerHolder, chunkIO, currentFilePath, fileSize, currentCleartextPath, lastModified, component, inUseManager, UseToken.CLOSED_TOKEN);
}


//for testing
OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, //
@CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, //
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
@OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component, //
InUseManager inUseManager, UseToken token) {
this.listener = listener;
this.cryptor = cryptor;
this.headerHolder = headerHolder;
this.chunkIO = chunkIO;
this.currentFilePath = currentFilePath;
this.currentCleartextPath = currentCleartextPath;
this.fileSize = fileSize;
this.component = component;
this.lastModified = lastModified;
Expand Down Expand Up @@ -180,6 +184,10 @@ public Path getCurrentFilePath() {
return currentFilePath.get();
}

public CryptoPath getCurrentCleartextPath() {
return currentCleartextPath.get();
}

/**
* Updates the current ciphertext file path, if it is not already set to null (i.e., the openCryptoFile is deleted)
*
Expand All @@ -190,10 +198,24 @@ public void updateCurrentFilePath(Path newFilePath) {
if (newFilePath != null) {
useToken.moveTo(newFilePath);
} else {
currentCleartextPath.set(null);
useToken.close(); //encrypted file will be deleted, hence we can stop checking usage
}
}

/**
* Updates the cleartext path if the file is not deleted (i.e., currentFilePath is not null).
* Null input is ignored.
*
* @param cleartextPath new cleartext path, or null to skip update
*/
public void updateCurrentCleartextPath(CryptoPath cleartextPath) {
if (cleartextPath == null) {
return;
}
currentCleartextPath.getAndUpdate(p -> currentFilePath.get() == null ? p : cleartextPath);
}

private synchronized void cleartextChannelClosed(FileChannel ciphertextFileChannel) {
if (ciphertextFileChannel != null) {
chunkIO.unregisterChannel(ciphertextFileChannel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dagger.Module;
import dagger.Provides;
import org.cryptomator.cryptofs.CryptoPath;

import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -33,6 +34,13 @@ public AtomicReference<Path> provideCurrentPath(@OriginalOpenFilePath Path origi
return new AtomicReference<>(originalPath);
}

@Provides
@OpenFileScoped
@CurrentOpenFileCleartextPath
public AtomicReference<CryptoPath> provideCurrentCleartextPath() {
return new AtomicReference<>(null);
}

@Provides
@OpenFileScoped
@OpenFileModifiedDate
Expand Down
26 changes: 19 additions & 7 deletions src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import jakarta.inject.Inject;
import org.cryptomator.cryptofs.CryptoFileSystemScoped;
import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.EffectiveOpenOptions;

import java.io.Closeable;
Expand Down Expand Up @@ -54,23 +55,30 @@ public Optional<OpenCryptoFile> get(Path ciphertextPath) {
* Opens a file to {@link OpenCryptoFile#newFileChannel(EffectiveOpenOptions, java.nio.file.attribute.FileAttribute[]) retrieve a FileChannel}. If this file is already opened, a shared instance is returned.
* Getting the file channel should be the next invocation, since the {@link OpenFileScoped lifecycle} of the OpenFile strictly depends on the lifecycle of the channel.
*
* @param cleartextPath Cleartext path of the file to open
* @param ciphertextPath Path of the file to open
* @return The opened file.
* @see #get(Path)
*/
public OpenCryptoFile getOrCreate(CryptoPath cleartextPath, Path ciphertextPath) {
OpenCryptoFile openFile = getOrCreate(ciphertextPath);
openFile.updateCurrentCleartextPath(cleartextPath);
return openFile;
}

public OpenCryptoFile getOrCreate(Path ciphertextPath) {
Path normalizedPath = ciphertextPath.toAbsolutePath().normalize();
return openCryptoFiles.computeIfAbsent(normalizedPath, p -> openCryptoFileComponentFactory.create(p, openCryptoFiles::remove).openCryptoFile()); // computeIfAbsent is atomic, "create" is called at most once
}

public void writeCiphertextFile(Path ciphertextPath, EffectiveOpenOptions openOptions, ByteBuffer contents) throws IOException {
try (OpenCryptoFile f = getOrCreate(ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
public void writeCiphertextFile(CryptoPath cleartextPath, Path ciphertextPath, EffectiveOpenOptions openOptions, ByteBuffer contents) throws IOException {
try (OpenCryptoFile f = getOrCreate(cleartextPath, ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
ch.write(contents);
}
}

public ByteBuffer readCiphertextFile(Path ciphertextPath, EffectiveOpenOptions openOptions, int maxBufferSize) throws BufferUnderflowException, IOException {
try (OpenCryptoFile f = getOrCreate(ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
public ByteBuffer readCiphertextFile(CryptoPath cleartextPath, Path ciphertextPath, EffectiveOpenOptions openOptions, int maxBufferSize) throws BufferUnderflowException, IOException {
try (OpenCryptoFile f = getOrCreate(cleartextPath, ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
if (ch.size() > maxBufferSize) {
throw new BufferUnderflowException();
}
Expand Down Expand Up @@ -101,11 +109,12 @@ public void delete(Path ciphertextPath) {
*
* @param src The ciphertext file path before the move
* @param dst The ciphertext file path after the move
* @param cleartextDst cleartext file path after the move
* @return Utility to update OpenCryptoFile references.
* @throws FileAlreadyExistsException Thrown if the destination file is an existing file that is currently opened.
*/
public TwoPhaseMove prepareMove(Path src, Path dst) throws FileAlreadyExistsException {
return new TwoPhaseMove(src, dst);
public TwoPhaseMove prepareMove(Path src, Path dst, CryptoPath cleartextDst) throws FileAlreadyExistsException {
return new TwoPhaseMove(src, dst, cleartextDst);
}

/**
Expand All @@ -125,13 +134,15 @@ public class TwoPhaseMove implements AutoCloseable {

private final Path src;
private final Path dst;
private final CryptoPath cleartextDst;
private final OpenCryptoFile openCryptoFile;
private boolean committed;
private boolean rolledBack;

private TwoPhaseMove(Path src, Path dst) throws FileAlreadyExistsException {
private TwoPhaseMove(Path src, Path dst, CryptoPath cleartextDst) throws FileAlreadyExistsException {
this.src = Objects.requireNonNull(src);
this.dst = Objects.requireNonNull(dst);
this.cleartextDst = cleartextDst;
try {
// ConcurrentHashMap.compute is atomic:
this.openCryptoFile = openCryptoFiles.compute(dst, (k, v) -> {
Expand All @@ -152,6 +163,7 @@ public void commit() {
}
if (openCryptoFile != null) {
openCryptoFile.updateCurrentFilePath(dst);
openCryptoFile.updateCurrentCleartextPath(cleartextDst);
}
openCryptoFiles.remove(src, openCryptoFile);
committed = true;
Expand Down
Loading
Loading