-
Notifications
You must be signed in to change notification settings - Fork 35
Feature: Files-in-Use #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 69 commits
f45b61e
2248ea8
751dcc7
3dd536b
a07bc23
c421f2a
9ac3c6a
da573e7
c336283
531300e
128b857
8de1fba
640f7aa
768ce65
eb49e42
880f0b8
40cb207
2a29ded
85f7ea9
6c071de
83388ad
0f13de9
8f443d8
f92b4e0
842b3c6
3d2a009
ce1ecd9
fb3cf20
22a83b1
1a722ea
aeb6f6c
2997f04
ac44892
80a8edb
a6d236e
dd79342
3bce755
05a326f
732bf7f
901f058
17319a3
775e0a0
b0b6947
1567acc
5517765
fcf468a
4699146
ca875c0
cb07aa9
c2edfbc
56305b2
71cc4d7
dcd6dab
c79f1e4
f30e5e0
3296a91
7c85167
10ecd47
4d5204a
57d3649
3888eba
83b596e
64d3e38
3a64f09
ee96d60
7dd7540
b6f0e4f
463d8dc
fc70e09
0960792
943503d
e5fb49e
5a89b48
8fb283b
d7c65ee
19e189c
270cf11
a9bd0a0
b40d183
fe9bbc7
b65c0ea
5eb9191
7a2b548
819a414
cd7bf09
c2089ab
7d3998a
bd9032f
3448469
ebe8c6e
eba5f29
fb7ccb9
a558887
75b3b50
fc0ac81
415cb76
cd64dd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| *******************************************************************************/ | ||||||||||||||||||||||||||||||||||||||||||||||||
| package org.cryptomator.cryptofs; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import jakarta.inject.Inject; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.attr.AttributeByNameProvider; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.attr.AttributeProvider; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.attr.AttributeViewProvider; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -19,10 +20,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.dir.DirectoryStreamFilters; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.event.FileIsInUseEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.event.FilesystemEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.fh.OpenCryptoFiles; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.inuse.FileAlreadyInUseException; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.inuse.InUseManager; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptofs.inuse.UseInfo; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import org.cryptomator.cryptolib.api.Cryptor; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import jakarta.inject.Inject; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.io.IOException; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.channels.FileChannel; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.file.AccessDeniedException; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,12 +61,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.file.attribute.PosixFileAttributes; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.file.attribute.PosixFilePermission; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.nio.file.attribute.UserPrincipalLookupService; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.time.Instant; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Arrays; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Collections; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.EnumSet; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Set; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.function.Consumer; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import static java.lang.String.format; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -92,10 +99,11 @@ class CryptoFileSystemImpl extends CryptoFileSystem { | |||||||||||||||||||||||||||||||||||||||||||||||
| private final CiphertextDirectoryDeleter ciphertextDirDeleter; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final ReadonlyFlag readonlyFlag; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final CryptoFileSystemProperties fileSystemProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private final InUseManager inUseManager; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final CryptoPath rootPath; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final CryptoPath emptyPath; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final FileNameDecryptor fileNameDecryptor; | ||||||||||||||||||||||||||||||||||||||||||||||||
| private final Consumer<FilesystemEvent> eventConsumer; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private volatile boolean open = true; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -105,7 +113,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems | |||||||||||||||||||||||||||||||||||||||||||||||
| PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup, // | ||||||||||||||||||||||||||||||||||||||||||||||||
| AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, // | ||||||||||||||||||||||||||||||||||||||||||||||||
| OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, // | ||||||||||||||||||||||||||||||||||||||||||||||||
| CryptoFileSystemProperties fileSystemProperties, FileNameDecryptor fileNameDecryptor) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| CryptoFileSystemProperties fileSystemProperties, InUseManager inUseManager, FileNameDecryptor fileNameDecryptor, Consumer<FilesystemEvent> eventConsumer) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.provider = provider; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.cryptoFileSystems = cryptoFileSystems; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.pathToVault = pathToVault; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -130,7 +138,9 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this.rootPath = cryptoPathFactory.rootFor(this); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.emptyPath = cryptoPathFactory.emptyFor(this); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.inUseManager = inUseManager; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.fileNameDecryptor = fileNameDecryptor; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.eventConsumer = eventConsumer; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -402,8 +412,9 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti | |||||||||||||||||||||||||||||||||||||||||||||||
| Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists | ||||||||||||||||||||||||||||||||||||||||||||||||
| FileChannel ch = null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.writable()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| ciphertextPath.persistLongFileName(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| stats.incrementAccessesWritten(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -414,7 +425,17 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti | |||||||||||||||||||||||||||||||||||||||||||||||
| stats.incrementAccesses(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return ch; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| ch.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (e instanceof FileAlreadyInUseException) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| var useInfo = inUseManager.getUseInfo(ciphertextFilePath).orElse(new UseInfo("UNKNOWN", Instant.now())); | ||||||||||||||||||||||||||||||||||||||||||||||||
| eventConsumer.accept(new FileIsInUseEvent(cleartextFilePath, ciphertextFilePath, useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(ciphertextFilePath))); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ch != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| ch.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (IOException closeEx) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| e.addSuppressed(closeEx); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -428,11 +449,18 @@ void delete(CryptoPath cleartextPath) throws IOException { | |||||||||||||||||||||||||||||||||||||||||||||||
| CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| switch (ciphertextFileType) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| case DIRECTORY -> deleteDirectory(cleartextPath, ciphertextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| case FILE, SYMLINK -> deleteFileOrSymlink(ciphertextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| case FILE -> deleteFile(cleartextPath, ciphertextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| case SYMLINK -> deleteSymlink(ciphertextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private void deleteFileOrSymlink(CiphertextFilePath ciphertextPath) throws IOException { | ||||||||||||||||||||||||||||||||||||||||||||||||
| private void deleteFile(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException { | ||||||||||||||||||||||||||||||||||||||||||||||||
| checkUsage(cleartextPath, ciphertextPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
| openCryptoFiles.delete(ciphertextPath.getFilePath()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| private void deleteSymlink(CiphertextFilePath ciphertextPath) throws IOException { | ||||||||||||||||||||||||||||||||||||||||||||||||
| openCryptoFiles.delete(ciphertextPath.getFilePath()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -605,6 +633,9 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co | |||||||||||||||||||||||||||||||||||||||||||||||
| CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); | ||||||||||||||||||||||||||||||||||||||||||||||||
| CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); | ||||||||||||||||||||||||||||||||||||||||||||||||
| try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| //TODO: skip this if owner is not set in Properties | ||||||||||||||||||||||||||||||||||||||||||||||||
| checkUsage(cleartextSource, ciphertextSource); | ||||||||||||||||||||||||||||||||||||||||||||||||
| checkUsage(cleartextTarget, ciphertextTarget); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (ciphertextTarget.isShortened()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| Files.createDirectories(ciphertextTarget.getRawPath()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| ciphertextTarget.persistLongFileName(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -700,4 +731,14 @@ public String toString() { | |||||||||||||||||||||||||||||||||||||||||||||||
| return format("%sCryptoFileSystem(%s)", open ? "" : "closed ", pathToVault); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| //visible for testing | ||||||||||||||||||||||||||||||||||||||||||||||||
| 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()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+736
to
+743
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use consistent ciphertext path (filePath) for event and exception isInUseByOthers() checks the file path; emitting the event and throwing the exception with rawPath is inconsistent and may confuse consumers. - 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);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,14 +11,19 @@ | |||||
| import org.cryptomator.cryptofs.attr.AttributeViewComponent; | ||||||
| import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; | ||||||
| import org.cryptomator.cryptofs.event.FilesystemEvent; | ||||||
| import org.cryptomator.cryptofs.inuse.StubInUseManager; | ||||||
| import org.cryptomator.cryptofs.inuse.InUseManager; | ||||||
| import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; | ||||||
| import org.cryptomator.cryptofs.inuse.RealInUseManager; | ||||||
| import org.cryptomator.cryptolib.api.Cryptor; | ||||||
| import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | ||||||
|
|
||||||
| import java.io.IOException; | ||||||
| import java.nio.file.FileStore; | ||||||
| import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | ||||||
| import java.util.Objects; | ||||||
| import java.util.Optional; | ||||||
| import java.util.function.Consumer; | ||||||
|
|
||||||
|
|
@@ -50,4 +55,16 @@ public Consumer<FilesystemEvent> provideFilesystemEventConsumer(CryptoFileSystem | |||||
| } | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| @Provides | ||||||
| @CryptoFileSystemScoped | ||||||
| public InUseManager provideInUseManager(CryptoFileSystemProperties fsProps, Cryptor cryptor) { | ||||||
| var owner = Objects.requireNonNullElse(fsProps.owner(),""); | ||||||
|
||||||
| var owner = Objects.requireNonNullElse(fsProps.owner(),""); | |
| var owner = Objects.requireNonNullElse(fsProps.owner(), ""); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package org.cryptomator.cryptofs.common; | ||
|
|
||
| import com.github.benmanes.caffeine.cache.Cache; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
|
|
||
| public abstract class CacheUtils { | ||
|
|
||
| private CacheUtils() {} | ||
|
|
||
| /** | ||
| * Helper function for caches with IO loading functions. | ||
| * <p> | ||
| * This method implements the famous workaround for (checked) IOExceptions in cache loading functions: | ||
| * {@code | ||
| * try { | ||
| * cache.get(key, k -> { | ||
| * try { | ||
| * return functionThrowingIOException(k) | ||
| * } catch (IOException e) { | ||
| * throw UncheckedIOException(e); | ||
| * } | ||
| * } | ||
| * } catch (UncheckedIOException e) { | ||
| * throw e.getCause() | ||
| * } | ||
| * } | ||
| * | ||
| * @param key The key to load from the cache | ||
| * @param cache The cache to get the value | ||
| * @param loadFunction The load function which can throw an IOException | ||
| * @return the cached value or null, if the loading function returns it. | ||
| * @param <K> The type of keys used in the cache | ||
| * @param <V> the type of values used in the cache | ||
| * @throws IOException if the loading function throws an IOException | ||
| */ | ||
| public static <K, V> V getWithIOWrapped(K key, Cache<K, V> cache, IOFunction<K, V> loadFunction) throws IOException { | ||
| try { | ||
| return cache.get(key, k -> { | ||
| try { | ||
| return loadFunction.apply(k); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| }); | ||
| } catch (UncheckedIOException e) { | ||
| throw e.getCause(); | ||
| } | ||
| } | ||
infeo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @FunctionalInterface | ||
| public interface IOFunction<T, R> { | ||
|
|
||
| R apply(T t) throws IOException; | ||
|
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO suggests an optimization: usage checks should be skipped when no owner is configured. This would prevent unnecessary work when the in-use feature is disabled.