Skip to content

Commit c46cf4e

Browse files
authored
Merge pull request #312 from cryptomator/feature/files-in-use
Feature: Files-in-Use
2 parents 35b71ba + 6a959cf commit c46cf4e

38 files changed

+2178
-85
lines changed

.idea/codeStyles/Project.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Changes to prior versions can be found on the [Github release page](https://gith
1010
## [Unreleased](https://github.com/cryptomator/cryptofs/compare/2.9.0...HEAD)
1111

1212
### Added
13-
* [changelog](CHANGELOG.md) file
13+
* Files-in-Use: Optional feature to indicate for external parties if an encrypted file is currently opened by this filesystem ([#312](https://github.com/cryptomator/cryptofs/pull/312))
14+
* Changelog file
1415

1516
### Changed
1617
* Use JDK 25 for build (bf26d6c9cd15a2489126ee0409a8ec9eca59da0c)

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<slf4j.version>2.0.17</slf4j.version>
2626

2727
<!-- test dependencies -->
28+
<awaitility.version>4.3.0</awaitility.version>
2829
<junit.jupiter.version>6.0.0</junit.jupiter.version>
2930
<jmh.version>1.37</jmh.version>
3031
<mockito.version>5.20.0</mockito.version>
@@ -159,6 +160,12 @@
159160
<version>${jimfs.version}</version>
160161
<scope>test</scope>
161162
</dependency>
163+
<dependency>
164+
<groupId>org.awaitility</groupId>
165+
<artifactId>awaitility</artifactId>
166+
<version>${awaitility.version}</version>
167+
<scope>test</scope>
168+
</dependency>
162169
</dependencies>
163170

164171
<build>

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*******************************************************************************/
99
package org.cryptomator.cryptofs;
1010

11+
import jakarta.inject.Inject;
1112
import org.cryptomator.cryptofs.attr.AttributeByNameProvider;
1213
import org.cryptomator.cryptofs.attr.AttributeProvider;
1314
import org.cryptomator.cryptofs.attr.AttributeViewProvider;
@@ -19,10 +20,14 @@
1920
import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter;
2021
import org.cryptomator.cryptofs.dir.DirectoryStreamFactory;
2122
import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
23+
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
24+
import org.cryptomator.cryptofs.event.FilesystemEvent;
2225
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
26+
import org.cryptomator.cryptofs.inuse.FileAlreadyInUseException;
27+
import org.cryptomator.cryptofs.inuse.InUseManager;
28+
import org.cryptomator.cryptofs.inuse.UseInfo;
2329
import org.cryptomator.cryptolib.api.Cryptor;
2430

25-
import jakarta.inject.Inject;
2631
import java.io.IOException;
2732
import java.nio.channels.FileChannel;
2833
import java.nio.file.AccessDeniedException;
@@ -56,12 +61,14 @@
5661
import java.nio.file.attribute.PosixFileAttributes;
5762
import java.nio.file.attribute.PosixFilePermission;
5863
import java.nio.file.attribute.UserPrincipalLookupService;
64+
import java.time.Instant;
5965
import java.util.Arrays;
6066
import java.util.Collections;
6167
import java.util.EnumSet;
6268
import java.util.Map;
6369
import java.util.Optional;
6470
import java.util.Set;
71+
import java.util.function.Consumer;
6572
import java.util.stream.Collectors;
6673

6774
import static java.lang.String.format;
@@ -92,10 +99,11 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
9299
private final CiphertextDirectoryDeleter ciphertextDirDeleter;
93100
private final ReadonlyFlag readonlyFlag;
94101
private final CryptoFileSystemProperties fileSystemProperties;
95-
102+
private final InUseManager inUseManager;
96103
private final CryptoPath rootPath;
97104
private final CryptoPath emptyPath;
98105
private final FileNameDecryptor fileNameDecryptor;
106+
private final Consumer<FilesystemEvent> eventConsumer;
99107

100108
private volatile boolean open = true;
101109

@@ -105,7 +113,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
105113
PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup, //
106114
AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, //
107115
OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, //
108-
CryptoFileSystemProperties fileSystemProperties, FileNameDecryptor fileNameDecryptor) {
116+
CryptoFileSystemProperties fileSystemProperties, InUseManager inUseManager, FileNameDecryptor fileNameDecryptor, Consumer<FilesystemEvent> eventConsumer) {
109117
this.provider = provider;
110118
this.cryptoFileSystems = cryptoFileSystems;
111119
this.pathToVault = pathToVault;
@@ -130,7 +138,9 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
130138

131139
this.rootPath = cryptoPathFactory.rootFor(this);
132140
this.emptyPath = cryptoPathFactory.emptyFor(this);
141+
this.inUseManager = inUseManager;
133142
this.fileNameDecryptor = fileNameDecryptor;
143+
this.eventConsumer = eventConsumer;
134144
}
135145

136146
@Override
@@ -203,9 +213,11 @@ public void close() throws IOException {
203213
open = false;
204214
finallyUtil.guaranteeInvocationOf( //
205215
() -> cryptoFileSystems.remove(this), //
206-
() -> openCryptoFiles.close(), //
207-
() -> directoryStreamFactory.close(), //
208-
() -> cryptor.destroy());
216+
openCryptoFiles::close, //
217+
directoryStreamFactory::close, //
218+
inUseManager::close, //
219+
cryptor::destroy //
220+
);
209221
}
210222
}
211223

@@ -402,8 +414,9 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
402414
Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists
403415
}
404416

405-
FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
417+
FileChannel ch = null;
406418
try {
419+
ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
407420
if (options.writable()) {
408421
ciphertextPath.persistLongFileName();
409422
stats.incrementAccessesWritten();
@@ -414,7 +427,17 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
414427
stats.incrementAccesses();
415428
return ch;
416429
} catch (Exception e) {
417-
ch.close();
430+
if (e instanceof FileAlreadyInUseException) {
431+
var useInfo = inUseManager.getUseInfo(ciphertextFilePath).orElse(new UseInfo("UNKNOWN", Instant.now()));
432+
eventConsumer.accept(new FileIsInUseEvent(cleartextFilePath, ciphertextFilePath, useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(ciphertextFilePath)));
433+
}
434+
if (ch != null) {
435+
try {
436+
ch.close();
437+
} catch (IOException closeEx) {
438+
e.addSuppressed(closeEx);
439+
}
440+
}
418441
throw e;
419442
}
420443
}
@@ -428,11 +451,18 @@ void delete(CryptoPath cleartextPath) throws IOException {
428451
CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath);
429452
switch (ciphertextFileType) {
430453
case DIRECTORY -> deleteDirectory(cleartextPath, ciphertextPath);
431-
case FILE, SYMLINK -> deleteFileOrSymlink(ciphertextPath);
454+
case FILE -> deleteFile(cleartextPath, ciphertextPath);
455+
case SYMLINK -> deleteSymlink(ciphertextPath);
432456
}
433457
}
434458

435-
private void deleteFileOrSymlink(CiphertextFilePath ciphertextPath) throws IOException {
459+
private void deleteFile(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException {
460+
checkUsage(cleartextPath, ciphertextPath);
461+
openCryptoFiles.delete(ciphertextPath.getFilePath());
462+
Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE);
463+
}
464+
465+
private void deleteSymlink(CiphertextFilePath ciphertextPath) throws IOException {
436466
openCryptoFiles.delete(ciphertextPath.getFilePath());
437467
Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE);
438468
}
@@ -605,6 +635,8 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
605635
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
606636
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
607637
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
638+
checkUsage(cleartextSource, ciphertextSource);
639+
checkUsage(cleartextTarget, ciphertextTarget);
608640
if (ciphertextTarget.isShortened()) {
609641
Files.createDirectories(ciphertextTarget.getRawPath());
610642
ciphertextTarget.persistLongFileName();
@@ -700,4 +732,14 @@ public String toString() {
700732
return format("%sCryptoFileSystem(%s)", open ? "" : "closed ", pathToVault);
701733
}
702734

735+
//visible for testing
736+
void checkUsage(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws FileAlreadyInUseException {
737+
var path = ciphertextPath.getFilePath();
738+
if (inUseManager.isInUseByOthers(path)) {
739+
var useInfo = inUseManager.getUseInfo(path).orElse(new UseInfo("UNKNOWN", Instant.now()));
740+
eventConsumer.accept(new FileIsInUseEvent(cleartextPath, ciphertextPath.getRawPath(), useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(path)));
741+
throw new FileAlreadyInUseException(ciphertextPath.getRawPath());
742+
}
743+
}
744+
703745
}

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
1313
import org.cryptomator.cryptofs.event.FilesystemEvent;
1414
import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
15+
import org.cryptomator.cryptofs.inuse.InUseManager;
16+
import org.cryptomator.cryptofs.inuse.RealInUseManager;
17+
import org.cryptomator.cryptofs.inuse.StubInUseManager;
18+
import org.cryptomator.cryptolib.api.Cryptor;
1519
import org.slf4j.Logger;
1620
import org.slf4j.LoggerFactory;
1721

1822
import java.io.IOException;
1923
import java.nio.file.FileStore;
2024
import java.nio.file.Files;
2125
import java.nio.file.Path;
26+
import java.util.Objects;
2227
import java.util.Optional;
2328
import java.util.function.Consumer;
2429

@@ -50,4 +55,16 @@ public Consumer<FilesystemEvent> provideFilesystemEventConsumer(CryptoFileSystem
5055
}
5156
};
5257
}
58+
59+
@Provides
60+
@CryptoFileSystemScoped
61+
public InUseManager provideInUseManager(CryptoFileSystemProperties fsProps, Cryptor cryptor) {
62+
var owner = Objects.requireNonNullElse(fsProps.owner(), "");
63+
if (!owner.isBlank() && !fsProps.readonly()) {
64+
return new RealInUseManager(owner, cryptor);
65+
} else {
66+
return new StubInUseManager();
67+
}
68+
69+
}
5370
}

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ public enum FileSystemFlags {
115115

116116
static final CryptorProvider.Scheme DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_GCM;
117117

118+
/**
119+
* Key identifying the filesystem owner.
120+
*
121+
* @since 2.10.0
122+
*/
123+
public static final String PROPERTY_OWNER = "owner";
124+
static final String DEFAULT_OWNER = "";
125+
118126
private final Set<Entry<String, Object>> entries;
119127

120128
private CryptoFileSystemProperties(Builder builder) {
@@ -126,7 +134,8 @@ private CryptoFileSystemProperties(Builder builder) {
126134
Map.entry(PROPERTY_EVENT_CONSUMER, builder.eventConsumer), //
127135
Map.entry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, builder.maxCleartextNameLength), //
128136
Map.entry(PROPERTY_SHORTENING_THRESHOLD, builder.shorteningThreshold), //
129-
Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) //
137+
Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo), //
138+
Map.entry(PROPERTY_OWNER, builder.owner) //
130139
);
131140
}
132141

@@ -169,6 +178,10 @@ Consumer<FilesystemEvent> filesystemEventConsumer() {
169178
return (Consumer<FilesystemEvent>) get(PROPERTY_EVENT_CONSUMER);
170179
}
171180

181+
String owner() {
182+
return (String) get(PROPERTY_OWNER);
183+
}
184+
172185
@Override
173186
public Set<Entry<String, Object>> entrySet() {
174187
return entries;
@@ -225,6 +238,7 @@ public static class Builder {
225238
private int maxCleartextNameLength = DEFAULT_MAX_CLEARTEXT_NAME_LENGTH;
226239
private int shorteningThreshold = DEFAULT_SHORTENING_THRESHOLD;
227240
private Consumer<FilesystemEvent> eventConsumer = DEFAULT_EVENT_CONSUMER;
241+
private String owner = DEFAULT_OWNER;
228242

229243
private Builder() {
230244
}
@@ -238,6 +252,7 @@ private Builder(Map<String, ?> properties) {
238252
checkedSet(Integer.class, PROPERTY_SHORTENING_THRESHOLD, properties, this::withShorteningThreshold);
239253
checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
240254
checkedSet(Consumer.class, PROPERTY_EVENT_CONSUMER, properties, this::withFilesystemEventConsumer);
255+
checkedSet(String.class, PROPERTY_OWNER, properties, this::withOwner);
241256
}
242257

243258
private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties, Consumer<T> setter) {
@@ -367,6 +382,23 @@ public Builder withFilesystemEventConsumer(Consumer<FilesystemEvent> eventConsum
367382
return this;
368383
}
369384

385+
/**
386+
* Sets the owner of the filesystem.
387+
* <p>
388+
* The owners length must be less than or equal to 100.
389+
*
390+
* @param owner the owner string used when marking files in-use
391+
* @return this
392+
* @since 2.10.0
393+
*/
394+
public Builder withOwner(String owner) {
395+
if (owner.length() > 100) {
396+
throw new IllegalArgumentException("owner must have length less than or equal to 100");
397+
}
398+
this.owner = owner;
399+
return this;
400+
}
401+
370402
/**
371403
* Validates the values and creates new {@link CryptoFileSystemProperties}.
372404
*

src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import jakarta.inject.Inject;
44
import org.cryptomator.cryptofs.common.Constants;
5+
import org.cryptomator.cryptofs.common.EncryptedChannels;
56
import org.cryptomator.cryptolib.api.CryptoException;
67
import org.cryptomator.cryptolib.api.Cryptor;
7-
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
8-
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
98

109
import java.io.IOException;
1110
import java.nio.ByteBuffer;
12-
import java.nio.channels.ByteChannel;
1311
import java.nio.charset.StandardCharsets;
1412
import java.nio.file.Files;
1513
import java.nio.file.Path;
@@ -38,7 +36,7 @@ public DirectoryIdBackup(Cryptor cryptor) {
3836
*/
3937
public void write(CiphertextDirectory ciphertextDirectory) throws IOException {
4038
try (var channel = Files.newByteChannel(getBackupFilePath(ciphertextDirectory.path()), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); //
41-
var encryptingChannel = wrapEncryptionAround(channel, cryptor)) {
39+
var encryptingChannel = EncryptedChannels.wrapEncryptionAround(channel, cryptor)) {
4240
encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId().getBytes(StandardCharsets.US_ASCII)));
4341
}
4442
}
@@ -72,7 +70,7 @@ public byte[] read(Path ciphertextContentDir) throws IOException, CryptoExceptio
7270
var dirIdBuffer = ByteBuffer.allocate(Constants.MAX_DIR_ID_LENGTH + 1); //a dir id contains at most 36 ascii chars, we add for security checks one more
7371

7472
try (var channel = Files.newByteChannel(dirIdBackupFile, StandardOpenOption.READ); //
75-
var decryptingChannel = wrapDecryptionAround(channel, cryptor)) {
73+
var decryptingChannel = EncryptedChannels.wrapDecryptionAround(channel, cryptor)) {
7674
int read = decryptingChannel.read(dirIdBuffer);
7775
if (read < 0 || read > Constants.MAX_DIR_ID_LENGTH) {
7876
throw new IllegalStateException("Read directory id exceeds the maximum length of %d characters".formatted(Constants.MAX_DIR_ID_LENGTH));
@@ -103,11 +101,4 @@ private static Path getBackupFilePath(Path ciphertextContentDir) {
103101
return ciphertextContentDir.resolve(Constants.DIR_ID_BACKUP_FILE_NAME);
104102
}
105103

106-
DecryptingReadableByteChannel wrapDecryptionAround(ByteChannel channel, Cryptor cryptor) {
107-
return new DecryptingReadableByteChannel(channel, cryptor, true);
108-
}
109-
110-
EncryptingWritableByteChannel wrapEncryptionAround(ByteChannel channel, Cryptor cryptor) {
111-
return new EncryptingWritableByteChannel(channel, cryptor);
112-
}
113104
}

0 commit comments

Comments
 (0)