Skip to content

Feature: Files-in-Use#312

Merged
infeo merged 106 commits intodevelopfrom
feature/files-in-use
Dec 9, 2025
Merged

Feature: Files-in-Use#312
infeo merged 106 commits intodevelopfrom
feature/files-in-use

Conversation

@infeo
Copy link
Member

@infeo infeo commented Oct 10, 2025

This PR implements the Files-in-use feature.

Problem

Cryptofs encrypts each file by its own, in order to be easily used for often synced files (i.e. synced with a cloud storage provider). A common scenario nowadays is, that multiple people work on the same synced files at the same time but at different locations. This leads to conflicts, which are normally resolved by the sync algorithms (and Cryptomator. But some applications simply overwrite newer versions without asking. Also, if the files reside on a network shared location, you have direct access and hence conflicts will not be detected.

Solution

Every time a file channel is opened or an existing file is overwritten/deleted, we first check if the file is currently used by another party. "Used" means, that an encrypted helper file with the same cipher file name and file extension .c9u exists, containing an owner string and a lastUpdated timestamp. If

  • the owner is not the same as this filesystem and
  • last updated time is recent enough (see below)

the file is considered as in use. If the file is used, io operations throw a special exception (FileAlreadyInUseException). Additionally, an event is triggered.

An in-use file is created, if a file channel with WRITE option is successfully opened to a file. The owner is set to the string handed over in the CryptoFileSystemProperties. The inUseFile has the same lifetime as the OpenCryptoFile and if the latter is closed, also the inUseFile is closed (and deleted). To mitigate performance drops, the first 5 seconds the inUseFile only exists in memory and afterwards it is persisted on disk. If during the "virtual" time the file channel is closed again , no file is created.

Every 5min, all open inUseFiles are refreshed, i.e. rewritten with an updated "lastUpdated" timestamp. If the lastUpdated time is 10min ago, it is considered stale and the file can be stolen by other users.

If a file is in-use , the consumer can "unblock" this state for the next accesses over a short amount of time. If during this time a file channel is openened, any existing inUseFile will be stolen, i.e. after successful opening the file is overwritten with this cryptofilesystem owner.

The whole inUse system is deactivated if no filesystem owner is specified in the CryptoFileSystemProperties. This is done by using a StubInUseManager.

Todos

  • Health check removing all in-use files
  • Conflict resolution of in-use-files

infeo added 30 commits July 30, 2025 13:41
* first read in-use-file
* on failure create/own it
* more static methods
* renaming methods
* rename FileIsInUseException to FileAlreadyInUseException
* rename InUse:tryMarkInUse to acquire()
* add ignore flag for inUse check
* close token on failed file creation
* add logging
* only move file if file is already created
* delete file on close
* add factory method to create InvalidToken
* add instance variable owner
* define in-use-file-creation threshold as a constant
* create interface
* sublass interface by "fake" impl and real impl
* decide on cryptofilesystem proeprties what impl to use
* create interface UseToken
* subclass it with real and stub impl
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (3)

28-29: Use ReentrantLock instead of WriteLock (read lock unused).

A plain ReentrantLock is simpler and clearer since only exclusive access is used.

-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReentrantLock;
@@
-	private final ReentrantReadWriteLock.WriteLock fileCreationSync = new ReentrantReadWriteLock().writeLock();
+	private final ReentrantLock fileCreationSync = new ReentrantLock();

Also applies to: 53-54


64-74: Rename ctor parameter for clarity (it’s a delayed executor).

The second constructor’s parameter named tokenPersistor is actually the executor used by runAsync (already delayed by the first ctor). Rename to avoid confusion.

-	RealUseToken(Path filePath, String owner, Cryptor cryptor, ConcurrentMap<Path, RealUseToken> useTokens, Executor tokenPersistor, OpenOption openMode, EncryptionDecorator encWrapper) {
+	RealUseToken(Path filePath, String owner, Cryptor cryptor, ConcurrentMap<Path, RealUseToken> useTokens, Executor executor, OpenOption openMode, EncryptionDecorator encWrapper) {
@@
-		this.creationTask = CompletableFuture.runAsync(() -> createInUseFile(openOptions), tokenPersistor);
+		this.creationTask = CompletableFuture.runAsync(() -> createInUseFile(openOptions), executor);

175-178: Log the exception cause on delete failure.

Currently the warn drops the IOException; include it for diagnostics.

-						//ignore
-						LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path);
+						LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path, e);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3448469 and ebe8c6e.

📒 Files selected for processing (1)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
🧬 Code graph analysis (1)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (3)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (11-41)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (40-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1)

178-203: Add exception to the log statement for better diagnostics.

The warning on line 195 should include the exception to aid troubleshooting.

Apply this diff:

 					} catch (IOException e) {
 						//ignore
-						LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path);
+						LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path, e);
 					}

Otherwise, the close logic is solid: the lock ensures atomicity, creationTask.cancel(false) is appropriate, and the differentiated deleteFile flag correctly supports both cleanup scenarios.

🧹 Nitpick comments (2)
src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1)

14-18: Consider renaming field to match type.

The field is named conflictRemover but the type is C9uConflictResolver. For consistency and clarity, consider renaming the field to conflictResolver.

Apply this diff:

-	private final C9uConflictResolver conflictRemover;
+	private final C9uConflictResolver conflictResolver;
 
 	@Inject
-	public C9uProcessor(C9uConflictResolver conflictRemover) {
-		this.conflictRemover = conflictRemover;
+	public C9uProcessor(C9uConflictResolver conflictResolver) {
+		this.conflictResolver = conflictResolver;
 	}

And update line 22:

 	public Stream<Node> process(Node node) {
-		return conflictRemover.process(node);
+		return conflictResolver.process(node);
 	}
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1)

46-46: Track or implement TODO for closing UseToken.

The TODO indicates that orphaned use tokens should be closed when their corresponding in-use files are deleted. This is related to the unused InUseManager field.

Do you want me to:

  1. Generate an implementation that uses InUseManager to close orphaned tokens?
  2. Open a new issue to track this task for future implementation?
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebe8c6e and fb7ccb9.

📒 Files selected for processing (9)
  • src/main/java/org/cryptomator/cryptofs/common/Constants.java (3 hunks)
  • src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (2 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java (3 hunks)
  • src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/inuse/RealUseTokenTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/cryptomator/cryptofs/common/Constants.java
  • src/test/java/org/cryptomator/cryptofs/inuse/RealUseTokenTest.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
🧬 Code graph analysis (6)
src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (1)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (5)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/main/java/org/cryptomator/cryptofs/common/StringUtils.java (1)
  • StringUtils (6-28)
src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java (1)
  • DirectoryStreamScoped (21-88)
src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1)
  • DirectoryStreamScoped (11-24)
src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (1)
  • DirectoryStreamScoped (8-36)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (3)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (40-255)
src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java (1)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (3)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-45)
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1)
  • DirectoryStreamScoped (20-57)
src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (1)
  • DirectoryStreamScoped (8-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test
🔇 Additional comments (12)
src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java (1)

3-3: LGTM! Good refactoring to use centralized constant.

The changes correctly replace the pattern reference with Constants.BASE64_PATTERN, improving maintainability by centralizing the definition. The import is properly added and the test logic remains unchanged.

Also applies to: 42-42, 57-57

src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1)

41-55: Base64 validation logic is correct.

The implementation properly validates the entire basename against the base64 pattern using matcher.matches(), appropriately handles deletion failures, and correctly returns an empty stream since in-use files should not appear in directory listings.

src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (2)

27-38: Test correctly validates valid base64 handling.

The test properly verifies that in-use files with valid base64-encoded basenames are preserved and the processor returns an empty stream.


40-51: Test correctly validates invalid base64 handling.

The test appropriately verifies that in-use files with invalid base64-encoded basenames (e.g., renamed with conflict markers) are deleted and the processor returns an empty stream.

src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (2)

13-22: Integration of C9uProcessor follows existing patterns.

The addition of C9uProcessor as a dependency is consistent with the existing architecture (similar to C9rProcessor and C9sProcessor). The constructor injection and field initialization are correct.


29-30: New processing branch correctly handles in-use files.

The implementation properly delegates in-use file processing to C9uProcessor and chains the result through brokenDirFilter, maintaining consistency with the existing processing branches for .c9r and .c9s files.

src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (6)

38-75: LGTM!

The factory methods and constructor are well-designed. The injected tokenPersistor executor properly addresses the resource leak from earlier reviews, and the delayed persistence mechanism is correctly implemented using CompletableFuture.delayedExecutor.


77-92: LGTM!

The synchronization and error handling are correct. The early return when closed is true prevents races with the close operation, and calling close() on IOException properly cleans up failed creation attempts.


94-115: Refresh logic is sound.

The modification detection (comparing lastModified timestamps) before refreshing is correct, and the differentiated close behavior makes sense:

  • ModifiedFileExceptionclose(false) preserves the file (someone else owns it)
  • IOExceptionclose() removes the file (can't maintain ownership)

The TODO on line 106 about event emission is reasonable given the lack of cleartext context at this layer.


117-132: LGTM!

The channel.truncate(0) at line 118 correctly addresses the trailing bytes concern from earlier reviews. The NonClosingByteChannel wrapper prevents premature closure, and channel.force(true) ensures durability. The lastModified update after the write properly tracks changes for subsequent refresh cycles.


168-171: LGTM!


205-237: LGTM!

The helper constructs are well-designed:

  • EncryptionDecorator provides a clean testability seam for mocking encryption
  • NonClosingByteChannel elegantly prevents premature closure of the long-lived FileChannel in try-with-resources blocks
  • ModifiedFileException serves as a clear marker for external modifications

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)

125-142: Add unit tests for validate() method.

The TODO comment and the corresponding note in RealInUseManagerTest.java (line 365) indicate that validate() lacks dedicated unit test coverage. This method performs critical validation (owner presence, blank check, timestamp parsing) and should be tested to ensure it correctly handles valid inputs, missing keys, blank owners, and malformed timestamps.

Would you like me to draft unit tests for this method?

src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (2)

187-192: Clarify or resolve the TODO regarding Files.move and lastModified.

If Files.move() alters the file's lastModified timestamp (behavior varies by platform/filesystem), the next refresh() call will detect the mismatch (line 141) and close the token without deleting the file (line 148 calls close(false)). This defensive behavior prevents orphaned files but may surprise users if moves frequently trigger closures.

Consider either:

  1. Documenting this behavior and removing the TODO, or
  2. Re-reading and updating lastModified after a successful move to keep the token synchronized:
 				if (channel != null) {
-					//TODO: does this affect the lastModified file?
 					Files.move(filePath, newFilePath, StandardCopyOption.REPLACE_EXISTING);
+					// Re-read lastModified to stay in sync after move
+					lastModified = Files.getLastModifiedTime(newFilePath).toMillis();
 				}

87-109: Consider adding explicit tests for scheduleRefresh logic.

The TODO comment on line 87 suggests missing test coverage. While testFileRefresh and related tests exercise refresh behavior, explicit unit tests for the exponential backoff scheduling (including edge cases like cancellation, closure during scheduling, and delay calculation) would improve coverage and confidence in this recursive scheduling logic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb7ccb9 and 75b3b50.

📒 Files selected for processing (6)
  • src/main/java/org/cryptomator/cryptofs/common/Constants.java (3 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/inuse/RealUseTokenTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/cryptomator/cryptofs/common/Constants.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
  • src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
🧬 Code graph analysis (3)
src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (3)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-241)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1)
  • RealUseToken (39-278)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (4)
src/main/java/org/cryptomator/cryptofs/common/CacheUtils.java (1)
  • CacheUtils (8-57)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1)
  • RealUseToken (39-278)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (3)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-241)
🔇 Additional comments (3)
src/test/java/org/cryptomator/cryptofs/inuse/RealUseTokenTest.java (1)

1-276: LGTM! Comprehensive test coverage.

The test suite thoroughly exercises RealUseToken behavior including delayed file creation, content validation, stealing scenarios, movement before/after creation, refresh logic, and closure. The use of WatchService to verify filesystem events and Awaitility for timing-based assertions is appropriate. Good job!

src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java (1)

1-35: LGTM! Clean interface design.

The sealed interface with default implementations, singleton tokens, and well-defined constants provides a clear contract for in-use token lifecycle management. The inline comment on line 34 explaining the MAX_CLEARTEXT_SIZE_BYTES calculation is helpful.

src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (1)

1-371: LGTM! Excellent test coverage.

The test suite comprehensively exercises RealInUseManager functionality including token creation/reuse, exception handling, file reading, owner/timestamp validation logic, and cache interactions. The TODO on line 365 for testing validate() is noted and addressed in the review of RealInUseManager.java. Well-organized test structure with nested classes and descriptive names.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1)

26-48: Finish UseToken cleanup before deleting invalid in-use files.

Right now the resolver deletes the renamed .c9u helper and returns, but the associated UseToken (if any) remains registered in InUseManager. That leaves a stale ownership entry behind, so subsequent open/overwrite attempts on the ciphertext still trip FileAlreadyInUseException until the entry times out. Please inject the InUseManager here and close/release the token before you delete the helper file so the in-use registry stays consistent, completing the TODO.

🧹 Nitpick comments (1)
src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java (1)

177-187: Consider clarifying the timing values.

The test correctly validates the in-use file lifecycle. The timing values (4900ms and 8000ms) correspond to the 5-second deferral mentioned in the PR objectives, but using magic numbers makes this less obvious.

Consider extracting timing values as constants or adding a comment:

+		// In-use files are deferred for 5 seconds, so we wait at least 4.9s for persistence
 		@Test
 		@DisplayName("Opening a file channel creates an in-use file and removes it on close")
 		public void testOpeningFCCreatesInUseFile() throws IOException {
 			try (var writer = FileChannel.open(file, CREATE, WRITE)) {
-				Awaitility.await().atLeast( 4900, TimeUnit.MILLISECONDS) //
-						.atMost(8000, TimeUnit.MILLISECONDS) //
+				Awaitility.await().atLeast(4900, TimeUnit.MILLISECONDS) //
+						.atMost(8000, TimeUnit.MILLISECONDS) //
 						.until(() -> numberOfInUseFiles() == 1);
 			}

Also note: the assertion on line 186 happens immediately after closing the channel, which assumes synchronous cleanup. This appears correct based on the PR description, but could be worth confirming if intermittent test failures occur in CI.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75b3b50 and 415cb76.

📒 Files selected for processing (4)
  • src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java (4 hunks)
  • src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.
Learnt from: infeo
PR: cryptomator/cryptofs#319
File: src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java:14-18
Timestamp: 2025-10-17T13:54:22.510Z
Learning: In the cryptofs event package (e.g., BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent), the event constructors are intended to be called only from within the filesystem implementation, so null-safety guards like Objects.requireNonNull() are not needed because the call sites are controlled and paths are guaranteed to be non-null.
📚 Learning: 2024-10-21T15:54:48.161Z
Learnt from: infeo
PR: cryptomator/cryptofs#253
File: src/test/java/org/cryptomator/cryptofs/ClearToCipherDirCacheTest.java:20-26
Timestamp: 2024-10-21T15:54:48.161Z
Learning: In the Java test file `ClearToCipherDirCacheTest.java`, the `beforeEach` method includes a call to `dirLoader.load()` which can throw an `IOException`, so the `throws IOException` declaration is necessary.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
  • src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
PR: cryptomator/cryptofs#312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java
🧬 Code graph analysis (3)
src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java (2)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java (1)
  • CryptoFileSystemProvider (86-322)
src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (3)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (1)
  • DirectoryStreamScoped (20-54)
src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (1)
  • DirectoryStreamScoped (8-36)
src/main/java/org/cryptomator/cryptofs/dir/C9uConflictResolver.java (4)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/common/StringUtils.java (1)
  • StringUtils (6-28)
src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1)
  • DirectoryStreamScoped (11-24)
src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java (1)
  • DirectoryStreamScoped (8-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test
🔇 Additional comments (5)
src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java (4)

12-13: LGTM!

The new imports are correctly added to support the in-use feature testing. Awaitility enables asynchronous waiting for the deferred in-use file creation, and Constants provides access to the in-use file suffix.


148-169: Well-structured test setup.

The setup correctly configures an in-memory filesystem with an owner property to activate the in-use feature. The use of Jimfs and mocked MasterkeyLoader aligns with other test classes in this file.


189-194: LGTM!

The helper method correctly walks the vault directory and counts in-use files. Proper resource management with try-with-resources ensures the stream is closed.


205-205: LGTM!

Refactoring vaultPath from a local variable to a field improves consistency with the new InUseFeatureActivated test class structure. The additional blank line enhances readability.

Also applies to: 210-210, 231-231

src/main/java/org/cryptomator/cryptofs/dir/C9uProcessor.java (1)

21-23: Delegating keeps the NodeProcessor flow consistent.

Routing .c9u handling straight through the conflict resolver mirrors the existing .c9r/.c9s paths and keeps the directory-processing pipeline tidy.

@infeo infeo requested a review from Copilot November 5, 2025 10:06
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an "in-use" feature for detecting and preventing concurrent file modifications across different filesystem instances. When a file is opened for writing, a .c9u (in-use) file is created containing encrypted ownership and timestamp information. The feature allows detection of files being used by other filesystem instances and prevents conflicts.

Key changes:

  • Introduces InUseManager interface and RealInUseManager implementation to manage in-use file markers
  • Adds RealUseToken class that creates delayed, encrypted in-use files with automatic refresh
  • Integrates in-use checks into file operations (open, move, delete) to throw FileAlreadyInUseException when conflicts are detected

Reviewed Changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/main/java/org/cryptomator/cryptofs/inuse/*.java Core in-use feature implementation including manager, tokens, and exception handling
src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java Integrates use token lifecycle with file channel operations
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java Adds usage checks for file operations and event emission
src/main/java/org/cryptomator/cryptofs/dir/C9u*.java Directory stream processing for .c9u files including conflict resolution
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java Adds owner property configuration with validation
src/test/java/**/*Test.java Comprehensive test coverage for new functionality
Files not reviewed (1)
  • .idea/codeStyles/Project.xml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Matcher matcher = BASE64_PATTERN.matcher(basename);
matcher.region(0, basename.length());
if (!matcher.matches()) { //any rename is considered bad
//TODO: close UseToken (if existent)
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO indicates a missing implementation: when a conflicted in-use file is deleted, the corresponding UseToken should be closed if it exists in the useTokens map. Without this, the token will remain open and continue to refresh a non-existent file.

Copilot uses AI. Check for mistakes.
}
writeInUseFile();
} catch (ModifiedFileException e) {
//TODO: event? we have no access to the cleartext!
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO highlights a limitation: when the in-use file is modified externally, no event is emitted because the cleartext path is unavailable. Consider storing the cleartext path reference or adding a separate event type for this scenario.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java (1)

17-18: Add @OverRide annotation to close() method.

The close() method overrides Closeable.close() and should be annotated with @Override for compile-time verification and clarity.

Apply this diff:

+	@Override
 	default void close() {}
🧹 Nitpick comments (3)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (3)

86-96: Address TODO: Add test coverage for refresh scheduling.

The TODO comment indicates missing or uncertain test coverage for the refresh scheduling mechanism. Given that this logic implements exponential backoff and chains persistence tasks, it's important to verify the behavior through tests.

Would you like me to help identify test scenarios for this method, such as:

  • Verifying the exponential backoff timing
  • Ensuring refresh stops when token is closed
  • Testing cancellation behavior

150-150: TODO indicates a known limitation: external modifications cannot emit events.

When the in-use file is externally modified, the token is closed (line 152) but no event can be emitted because the cleartext path is not available in this context. This is a minor observability gap.

Would you like me to open an issue to track this limitation, or is this an acceptable behavior given the low likelihood of external modification to encrypted in-use files?


237-240: Include exception in log statement for better diagnostics.

The IOException is caught but not passed to the logger, losing valuable diagnostic information such as the stack trace and root cause.

Apply this diff:

 				} catch (IOException e) {
 					//ignore
-					LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path);
+					LOG.warn("Failed to delete inUse File {}. Must be deleted manually.", path, e);
 				}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd64dd0 and 6bcf079.

📒 Files selected for processing (3)
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java (2 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 319
File: src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java:14-18
Timestamp: 2025-10-17T13:54:22.510Z
Learning: In the cryptofs event package (e.g., BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent), the event constructors are intended to be called only from within the filesystem implementation, so null-safety guards like Objects.requireNonNull() are not needed because the call sites are controlled and paths are guaranteed to be non-null.
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
  • src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
  • src/main/java/org/cryptomator/cryptofs/inuse/UseToken.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2025-10-17T13:54:22.510Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 319
File: src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java:14-18
Timestamp: 2025-10-17T13:54:22.510Z
Learning: In the cryptofs event package (e.g., BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent), the event constructors are intended to be called only from within the filesystem implementation, so null-safety guards like Objects.requireNonNull() are not needed because the call sites are controlled and paths are guaranteed to be non-null.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2024-12-10T12:28:49.092Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 263
File: src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java:53-54
Timestamp: 2024-12-10T12:28:49.092Z
Learning: In the `FileNameDecryptor` class (`org.cryptomator.cryptofs`), the `validatePath()` method ensures that filenames have a minimum length of 26 characters. Therefore, in `decryptFilenameInternal()`, it's safe to perform substring operations like `fullCipherNodeName.substring(fullCipherNodeName.length() - 4)` without additional length checks.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
🧬 Code graph analysis (2)
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java (2)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-241)
src/main/java/org/cryptomator/cryptofs/inuse/StubInUseManager.java (1)
  • StubInUseManager (5-11)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (2)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-241)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test
🔇 Additional comments (2)
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java (2)

15-18: LGTM: Imports correctly support the new provider.

All new imports are necessary for the provideInUseManager implementation and properly organized.

Also applies to: 26-26


59-69: LGTM: Provider logic correctly implements conditional in-use tracking.

The implementation correctly returns RealInUseManager only when both conditions are met (non-blank owner AND writable filesystem), otherwise falling back to StubInUseManager. The defensive null-check with Objects.requireNonNullElse is good practice, and isBlank() appropriately excludes empty or whitespace-only owners.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java (1)

735-743: Inconsistent path usage between check and event/exception.

The method checks inUseManager.isInUseByOthers(path) using ciphertextPath.getFilePath(), but emits the event and throws the exception using ciphertextPath.getRawPath(). This inconsistency was noted in a past review but appears unaddressed.

Consider using consistent paths:

 void checkUsage(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws FileAlreadyInUseException {
 	var path = ciphertextPath.getFilePath();
 	if (inUseManager.isInUseByOthers(path)) {
 		var useInfo = inUseManager.getUseInfo(path).orElse(new UseInfo("UNKNOWN", Instant.now()));
-		eventConsumer.accept(new FileIsInUseEvent(cleartextPath, ciphertextPath.getRawPath(), useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(path)));
-		throw new FileAlreadyInUseException(ciphertextPath.getRawPath());
+		eventConsumer.accept(new FileIsInUseEvent(cleartextPath, path, useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(path)));
+		throw new FileAlreadyInUseException(path);
 	}
 }
🧹 Nitpick comments (1)
src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1)

36-47: Minor naming/style nits and potential helper extraction

Functionally this test looks good: it mirrors the valid‑case setup, injects an invalid (conflicted copy) fragment to violate BASE64_PATTERN, and asserts the .c9u file gets removed.

Two small, optional polish points:

  • Rename InvalidBase64Deleted to start with a lowercase letter (invalidBase64Deleted) to follow Java method naming conventions.
  • Consider extracting a tiny helper like createInUseNode(Path dir, String namePrefix) to remove duplication between the two tests and make the intent (“given a .c9u node with this prefix…”) even clearer.

These are non‑blocking niceties only.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 464f186 and 2be8354.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java (12 hunks)
  • src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1 hunks)
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java (12 hunks)
  • src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1 hunks)
  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java (8 hunks)
  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java (0 hunks)
  • src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (1 hunks)
💤 Files with no reviewable changes (1)
  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 319
File: src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java:14-18
Timestamp: 2025-10-17T13:54:22.510Z
Learning: In the cryptofs event package (e.g., BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent), the event constructors are intended to be called only from within the filesystem implementation, so null-safety guards like Objects.requireNonNull() are not needed because the call sites are controlled and paths are guaranteed to be non-null.
📚 Learning: 2025-10-14T16:55:45.287Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java:44-46
Timestamp: 2025-10-14T16:55:45.287Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/InUseManager.java`, the interface includes default implementations (including `use()` returning `INIT_TOKEN`) because there are only two implementations—StubInUseManager (stub) and RealInUseManager (real)—with no plans for additional implementations. This design is intentional and acceptable.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
  • src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java
  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
📚 Learning: 2025-10-17T13:49:30.221Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 312
File: src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java:108-118
Timestamp: 2025-10-17T13:49:30.221Z
Learning: In `src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java`, the owner field is validated in `CryptoFileSystemProperties.withOwner(String)` to enforce a maximum length of 100 characters. This validation ensures that the serialized Properties payload (owner + lastUpdated timestamp + Properties overhead) never exceeds the 1000-byte INUSE_CLEARTEXT_SIZE buffer, making additional size checks in writeInUseFile() unnecessary.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
  • src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java
📚 Learning: 2024-10-21T15:54:48.161Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 253
File: src/test/java/org/cryptomator/cryptofs/ClearToCipherDirCacheTest.java:20-26
Timestamp: 2024-10-21T15:54:48.161Z
Learning: In the Java test file `ClearToCipherDirCacheTest.java`, the `beforeEach` method includes a call to `dirLoader.load()` which can throw an `IOException`, so the `throws IOException` declaration is necessary.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
📚 Learning: 2025-10-17T13:54:22.510Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 319
File: src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java:14-18
Timestamp: 2025-10-17T13:54:22.510Z
Learning: In the cryptofs event package (e.g., BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent), the event constructors are intended to be called only from within the filesystem implementation, so null-safety guards like Objects.requireNonNull() are not needed because the call sites are controlled and paths are guaranteed to be non-null.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
📚 Learning: 2024-10-21T15:56:44.806Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 253
File: src/test/java/org/cryptomator/cryptofs/ClearToCipherDirCacheTest.java:115-115
Timestamp: 2024-10-21T15:56:44.806Z
Learning: In the Java test file `ClearToCipherDirCacheTest.java`, within the `entryUntouchedOnPrefixFailure` test method, the `dirLoader.load()` method is expected to be called twice, corresponding to invocations at lines 110 and 113.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
📚 Learning: 2024-10-22T08:36:41.464Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 253
File: src/main/java/org/cryptomator/cryptofs/ClearToCipherDirCache.java:59-68
Timestamp: 2024-10-22T08:36:41.464Z
Learning: In the `ClearToCipherDirCache` class, recursive writes to the cache are not allowed because the `CipherDirLoader` might alter the cache by recursive computation. Therefore, computation is performed outside of the atomic context, as suggested in the Caffeine wiki.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
📚 Learning: 2024-12-10T12:28:49.092Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 263
File: src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java:53-54
Timestamp: 2024-12-10T12:28:49.092Z
Learning: In the `FileNameDecryptor` class (`org.cryptomator.cryptofs`), the `validatePath()` method ensures that filenames have a minimum length of 26 characters. Therefore, in `decryptFilenameInternal()`, it's safe to perform substring operations like `fullCipherNodeName.substring(fullCipherNodeName.length() - 4)` without additional length checks.

Applied to files:

  • src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java
📚 Learning: 2024-10-21T15:52:40.091Z
Learnt from: infeo
Repo: cryptomator/cryptofs PR: 253
File: src/test/java/org/cryptomator/cryptofs/ClearToCipherDirCacheTest.java:55-63
Timestamp: 2024-10-21T15:52:40.091Z
Learning: In the Java test file `ClearToCipherDirCacheTest.java`, within the `entryRemovedOnPrefixSuccess` test method, the cache miss is confirmed by verifying that the loader is triggered again, so adding an assertion to check the cache state is unnecessary.

Applied to files:

  • src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
🧬 Code graph analysis (5)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (4)
src/main/java/org/cryptomator/cryptofs/common/CacheUtils.java (1)
  • CacheUtils (8-57)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1)
  • RealUseToken (38-282)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (2)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-242)
src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (3)
src/main/java/org/cryptomator/cryptofs/common/EncryptedChannels.java (1)
  • EncryptedChannels (9-22)
src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (1)
  • RealInUseManager (38-242)
src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (1)
  • RealUseToken (38-282)
src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java (2)
src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java (1)
  • TwoPhaseMove (124-174)
src/main/java/org/cryptomator/cryptofs/inuse/FileAlreadyInUseException.java (1)
  • FileAlreadyInUseException (6-11)
src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (1)
src/main/java/org/cryptomator/cryptofs/common/Constants.java (1)
  • Constants (13-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test
🔇 Additional comments (47)
CHANGELOG.md (2)

13-14: Changelog entries clearly document the Files-in-Use feature.

The new entries accurately describe the feature's purpose ("indicate for external parties if an encrypted file is currently opened") and reference the correct PR. Formatting is consistent with the changelog style.


17-18: Inconsistency between AI summary and provided code.

The AI summary states that "Pin external ci actions (#320)" should be added (line 18), but the provided code shows line 18 without a ~ mark, indicating it was not changed in this PR. Verify whether line 18 is a new addition or pre-existing content.

src/test/java/org/cryptomator/cryptofs/dir/C9uConflictResolverTest.java (2)

16-22: Good, minimal setup of C9uConflictResolver per test

Using @BeforeEach to create a fresh C9uConflictResolver keeps tests isolated and simple; no issues here.


23-35: Valid base64 test nicely reflects expected retention behavior

The validBase64KeepsExisting test clearly expresses that a syntactically valid base64 prefix plus INUSE_FILE_SUFFIX must not be deleted, and @TempDir keeps it side‑effect free. The chosen string matches the BASE64_PATTERN contract, so this is a solid, targeted test.

src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java (8)

108-109: LGTM: New mock fields for in-use workflow.

The new eventConsumer and inUseManager mock fields are correctly declared and align with the constructor changes in CryptoFileSystemImpl.


127-132: LGTM: Constructor wiring includes new in-use dependencies.

The test constructor call correctly passes inUseManager and eventConsumer to align with the production constructor signature.


291-295: LGTM: FinallyUtil stub expanded for inUseManager::close.

The guaranteeInvocationOf stub now accepts 5 RunnableThrowingException arguments, matching the updated close() implementation that includes inUseManager::close.

Also applies to: 367-371


305-310: LGTM: Test verifies inUseManager.close() is invoked during shutdown.

This test ensures proper resource cleanup by verifying inUseManager.close() is called when the filesystem closes.


457-488: LGTM: checkUsage tests cover in-use and not-in-use scenarios.

Both test cases properly verify:

  • FileAlreadyInUseException is thrown when file is in-use
  • FileIsInUseEvent is emitted via eventConsumer
  • No event is emitted when file is not in-use

604-612: LGTM: newFileChannel in-use failure test.

The test correctly verifies that when OpenCryptoFile.newFileChannel throws FileAlreadyInUseException, a FileIsInUseEvent is emitted.


653-700: LGTM: Delete tests properly verify in-use checks.

The tests cover:

  • Regular file deletion with checkUsage verification
  • Shortened file deletion skipping in-use file deletion
  • FileAlreadyInUseException preventing deletion

874-917: LGTM: Move tests verify in-use checks for source and destination.

The tests properly verify that checkUsage is called for both source and destination paths during file moves, and that FileAlreadyInUseException prevents the move operation.

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java (5)

102-106: LGTM: New in-use management fields.

The inUseManager and eventConsumer fields are correctly declared for integrating the in-use workflow.


214-220: LGTM: close() properly includes inUseManager cleanup.

The inUseManager::close is correctly added to the finallyUtil.guaranteeInvocationOf chain, ensuring executor shutdown during filesystem close.


429-442: LGTM: FileAlreadyInUseException handling with proper exception suppression.

The exception handling correctly:

  1. Emits FileIsInUseEvent when FileAlreadyInUseException occurs
  2. Safely closes the channel with suppressed exception handling (addressing the past review comment)

459-467: LGTM: File deletion refactored to include usage check.

The deleteFile method correctly calls checkUsage before proceeding with deletion, ensuring files in use by others are not deleted.


638-639: LGTM: Move operation checks usage for both source and target.

Correctly verifies both source and destination files are not in use before performing the move.

src/main/java/org/cryptomator/cryptofs/inuse/RealInUseManager.java (10)

51-65: LGTM: Constructor properly initializes caches and executor.

The constructor correctly sets up:

  • ignoredInUseFiles cache with 2-minute TTL and max 100 entries
  • useInfoCache with 5-second TTL and max 1000 entries
  • Virtual thread executor for async token persistence

68-80: LGTM: isInUseByOthers short-circuits when token exists.

The method correctly returns false immediately if the path exists in useTokens (meaning this filesystem owns it), avoiding unnecessary I/O.


95-106: LGTM: isInUse respects ignored cache and uses useInfoCache.

The method correctly:

  1. Returns false if the file is in the ignored cache
  2. Uses CacheUtils.getWithIOWrapped to load from cache or file

108-125: LGTM: readInUseFile decrypts and parses in-use content.

The method correctly reads encrypted content, validates non-empty, and parses into Properties.


127-143: LGTM: validate extracts owner from file content.

The validation correctly reads owner from the Properties (addressing the past critical review about using local owner), validates required keys, and parses the timestamp.


145-153: LGTM: isInUse(UseInfo) correctly compares ownership and freshness.

The method properly returns false for same-owner files and checks the stale threshold for different owners.


168-183: LGTM: use() handles token creation and exceptions appropriately.

The method correctly:

  1. Uses computeIfAbsent for atomic token creation
  2. Invalidates the ignored cache entry
  3. Rethrows FileAlreadyInUseException
  4. Returns CLOSED_TOKEN for other IO errors

185-200: LGTM: createInternal handles all file states correctly.

The method properly handles:

  • Existing valid in-use file owned by others → FileAlreadyInUseException
  • Existing valid file not in use → createWithExistingFile
  • Non-existing file → createWithNewFile
  • Invalid file → createWithExistingFile (take ownership)

209-221: LGTM: close() properly shuts down executor.

The shutdown sequence correctly:

  1. Calls shutdown()
  2. Awaits termination with 5-second timeout
  3. Forces shutdownNow() if timeout or interrupted
  4. Re-interrupts thread on InterruptedException

This addresses the past review about resource leaks.


227-231: LGTM: computeInUseFilePath correctly derives in-use file path.

The method properly replaces .c9r suffix with .c9u to compute the in-use file path.

src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java (6)

67-68: LGTM: New mock fields for in-use workflow.

The inUseManager mock and useToken spy (on INIT_TOKEN since it's a sealed class) are correctly declared for testing the in-use lifecycle.


81-93: LGTM: Test setup and helper method.

The beforeEach correctly initializes the spy's default behavior, and getTestInstance properly creates test instances with all required dependencies including the in-use components.


95-104: LGTM: close() test verifies token cleanup.

The test correctly verifies that closing an OpenCryptoFile triggers both the closeListener and useToken.close().


122-163: LGTM: Token reuse and read-only tests.

These tests properly verify:

  • Open token is not reassigned (inUseManager.use not called)
  • Read-only file access skips usage checks entirely

165-200: LGTM: Closed token and in-use exception tests.

The tests correctly verify:

  • Closed token triggers inUseManager.use() to get a new one
  • FileAlreadyInUseException propagates when file is in use

247-270: LGTM: updateCurrentFilePath tests.

The tests correctly verify:

  • Non-null path triggers useToken.moveTo(newPath)
  • Null path triggers useToken.close()
src/test/java/org/cryptomator/cryptofs/inuse/RealInUseManagerTest.java (9)

44-52: LGTM: Test setup with proper mock initialization.

The beforeEach correctly sets up mocks and the static mock for computeInUseFilePath, ensuring consistent path resolution across tests.


54-93: LGTM: isInUseByOthers tests cover all paths.

The tests properly verify:

  • Returns false when token exists in map
  • Delegates to isInUse when not in map
  • Returns false on IllegalArgumentException or IOException

95-138: LGTM: use() tests verify token creation and exception handling.

The tests correctly verify:

  • Token is stored in map and returned
  • FileAlreadyInUseException is rethrown
  • IOException results in CLOSED_TOKEN

140-218: LGTM: createInternal tests cover all scenarios.

The tests properly verify:

  • Existing valid file → createWithExistingFile
  • Invalid file → createWithExistingFile (take ownership)
  • Non-existing file → createWithNewFile
  • In-use file → throws UncheckedIOException(FileAlreadyInUseException)

220-293: LGTM: ReadInUseFile tests cover success and error cases.

The nested tests properly verify:

  • Successful read with decryption and Properties parsing
  • NoSuchFileException for non-existing files
  • IllegalArgumentException for empty files

295-331: LGTM: IsInUse(UseInfo) tests verify owner and timestamp logic.

The tests correctly verify:

  • Same owner → false
  • Different owner, recent timestamp → true
  • Different owner, stale timestamp → false

333-363: LGTM: Cache interaction tests.

The tests properly verify that isInUse consults both the ignored cache and useInfo cache correctly.


365-392: LGTM: Comprehensive validate() tests.

The test covers all validation scenarios:

  • Missing owner
  • Blank owner
  • Missing lastUpdated
  • Invalid lastUpdated format
  • Valid properties

394-397: LGTM: Proper cleanup of static mock.

The afterEach correctly closes the static mock to prevent interference between tests.

src/main/java/org/cryptomator/cryptofs/inuse/RealUseToken.java (5)

40-52: Clean factory design and reasonable concurrency limits.

The factory methods clearly separate new file creation from existing file takeover, and the static semaphore (20 permits) appropriately throttles concurrent write operations across all tokens.


86-119: Refresh scheduling and exponential backoff logic are sound.

The token lifecycle correctly chains delayed tasks: initial creation after 5 seconds, then periodic refreshes with exponential backoff (15s → 30s → 60s → 120s → 240s → capped at 300s). The semaphore acquisition is properly guarded with a finally block to prevent permit leaks.


138-176: Refresh and write logic properly handle concurrency and persistence.

The refresh() method correctly detects external modifications by comparing lastModified timestamps and closes the token on mismatch. The writeInUseFile() method addresses past concerns by truncating to zero before writing, and the NonClosingByteChannel wrapper cleverly prevents premature closure of the underlying file channel during encryption operations.


184-210: Move operation correctly handles map updates and relies on refresh for validation.

The atomic map update using compute() is sound, and the comment on line 194 explains the strategy: if Files.move() unexpectedly changes lastModified (rare, but possible), the next refresh() will detect the mismatch and close the token. This is a pragmatic approach that avoids platform-specific workarounds.


249-282: Well-designed helper types for testability and channel lifecycle.

The EncryptionDecorator interface enables test injection, and the NonClosingByteChannel record elegantly prevents premature closure of the file channel during try-with-resources blocks. The ModifiedFileException provides clear intent when external modifications are detected.

@infeo infeo merged commit c46cf4e into develop Dec 9, 2025
3 checks passed
@infeo infeo added this to the next milestone Dec 11, 2025
@infeo
Copy link
Member Author

infeo commented Dec 11, 2025

Addendum: The interface for setting the owner was changed in 6d75e7b due to downstream limitations.

For Cryptomator Hub vaults an online authentication is required. This is only done when loading the key. Hence, beforehand there is no possibility to receive the unlocking hub user. But at keyloading time, the operation is already building the filesystem with the given filesystem properties. Hence, the interface is changed to set now a Supplier<String> fsOwnerGetter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants