diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf21af27c..f58cafe15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,8 +53,9 @@ jobs: body: |- :construction: Work in Progress - Please be patient, the builds are still running. We will publish new versions of Cryptomator here in a few moments. + ⏳ Please be patient, the builds are still [running](https://github.com/cryptomator/cryptomator/actions). New versions of Cryptomator can be found here in a few moments. ⏳ As usual, the GPG signatures can be checked using [our public key `5811 7AFA 1F85 B3EE C154 677D 615D 449F E6E6 A235`](https://gist.github.com/cryptobot/211111cf092037490275f39d408f461a). + --- \ No newline at end of file diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index 107261f2b..036798650 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -66,6 +66,7 @@ + diff --git a/pom.xml b/pom.xml index d6de06b01..14674b42f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator cryptomator - 1.9.0-SNAPSHOT + 1.10.0-SNAPSHOT Cryptomator Desktop App @@ -33,32 +33,32 @@ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents - 2.6.2 + 2.6.4 1.3.0-beta1 1.2.0 1.2.0 1.3.0-beta3 3.0.0 2.0.0 - 2.0.2 + 2.0.3 3.12.0 2.45 2.2 - 31.1-jre + 32.0.0-jre 2.10.1 20.0.1 - 4.3.0 + 4.4.0 9.31 - 1.4.5 - 2.0.6 + 1.4.7 + 2.0.7 0.5.1 1.7.0 - 5.9.2 - 5.1.1 + 5.9.3 + 5.3.1 2.2 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a4a71a2cc..54938a0f0 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,4 +1,16 @@ import ch.qos.logback.classic.spi.Configurator; +import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider; +import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider; +import org.cryptomator.common.locationpresets.DropboxWindowsLocationPresetsProvider; +import org.cryptomator.common.locationpresets.GoogleDriveLocationPresetsProvider; +import org.cryptomator.common.locationpresets.ICloudMacLocationPresetsProvider; +import org.cryptomator.common.locationpresets.ICloudWindowsLocationPresetsProvider; +import org.cryptomator.common.locationpresets.LocationPresetsProvider; +import org.cryptomator.common.locationpresets.MegaLocationPresetsProvider; +import org.cryptomator.common.locationpresets.OneDriveLinuxLocationPresetsProvider; +import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider; +import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider; +import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider; import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.logging.LogbackConfiguratorFactory; import org.cryptomator.ui.traymenu.AwtTrayMenuController; @@ -37,6 +49,15 @@ open module org.cryptomator.desktop { /* TODO: filename-based modules: */ requires static javax.inject; /* ugly dagger/guava crap */ + uses org.cryptomator.common.locationpresets.LocationPresetsProvider; + provides TrayMenuController with AwtTrayMenuController; provides Configurator with LogbackConfiguratorFactory; + provides LocationPresetsProvider with DropboxMacLocationPresetsProvider, // + DropboxWindowsLocationPresetsProvider, DropboxLinuxLocationPresetsProvider, // + ICloudMacLocationPresetsProvider, ICloudWindowsLocationPresetsProvider, // + GoogleDriveLocationPresetsProvider, // + PCloudLocationPresetsProvider, MegaLocationPresetsProvider, // + OneDriveLinuxLocationPresetsProvider, OneDriveWindowsLocationPresetsProvider, // + OneDriveMacLocationPresetsProvider; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/common/LocationPreset.java b/src/main/java/org/cryptomator/common/LocationPreset.java deleted file mode 100644 index 7861c6266..000000000 --- a/src/main/java/org/cryptomator/common/LocationPreset.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.cryptomator.common; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; - -/** - * Enum of common cloud providers and their default local storage location path. - */ -public enum LocationPreset { - - DROPBOX("Dropbox", "~/Library/CloudStorage/Dropbox", "~/Dropbox"), - ICLOUDDRIVE("iCloud Drive", "~/Library/Mobile Documents/com~apple~CloudDocs", "~/iCloudDrive"), - GDRIVE("Google Drive", "~/Google Drive/My Drive", "~/Google Drive"), - MEGA("MEGA", "~/MEGA"), - ONEDRIVE("OneDrive", "~/OneDrive"), - PCLOUD("pCloud", "~/pCloudDrive"), - - LOCAL("local"); - - private final String name; - private final List candidates; - - LocationPreset(String name, String... candidates) { - this.name = name; - this.candidates = Arrays.stream(candidates).map(UserHome::resolve).map(Path::of).toList(); - } - - /** - * Checks for this LocationPreset if any of the associated paths exist. - * - * @return the first existing path or null, if none exists. - */ - public Path existingPath() { - return candidates.stream().filter(Files::isDirectory).findFirst().orElse(null); - } - - public String getDisplayName() { - return name; - } - - @Override - public String toString() { - return getDisplayName(); - } - - //this contruct is needed, since static members are initialized after every enum member is initialized - //TODO: refactor this to normal class and use this also in different parts of the project - private static class UserHome { - - private static final String USER_HOME = System.getProperty("user.home"); - - private static String resolve(String path) { - if (path.startsWith("~/")) { - return UserHome.USER_HOME + path.substring(1); - } else { - return path; - } - } - } - -} - diff --git a/src/main/java/org/cryptomator/common/locationpresets/DropboxLinuxLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/DropboxLinuxLocationPresetsProvider.java new file mode 100644 index 000000000..ce159a019 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/DropboxLinuxLocationPresetsProvider.java @@ -0,0 +1,32 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.OperatingSystem; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.LINUX; + +@OperatingSystem(LINUX) +public final class DropboxLinuxLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path USER_HOME = LocationPresetsProvider.resolveLocation("~/.").toAbsolutePath(); + private static final Predicate PATTERN = Pattern.compile("Dropbox \\(.+\\)").asMatchPredicate(); + + @Override + public Stream getLocations() { + try (var dirStream = Files.list(USER_HOME)) { + var presets = dirStream.filter(p -> Files.isDirectory(p) && PATTERN.test(p.getFileName().toString())) // + .map(p -> new LocationPreset(p.getFileName().toString(), p)) // + .toList(); + return presets.stream(); //workaround to ensure that the directory stream is always closed + } catch (IOException | UncheckedIOException e) { //UncheckedIOException thrown by the stream of Files.list() + return Stream.of(); + } + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/DropboxMacLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/DropboxMacLocationPresetsProvider.java new file mode 100644 index 000000000..341a3857c --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/DropboxMacLocationPresetsProvider.java @@ -0,0 +1,35 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; + +@OperatingSystem(MAC) +@CheckAvailability +public final class DropboxMacLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Library/CloudStorage/Dropbox"); + private static final Path FALLBACK_LOCATION = LocationPresetsProvider.resolveLocation("~/Dropbox"); + + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION) || Files.isDirectory(FALLBACK_LOCATION); + } + + @Override + public Stream getLocations() { + if(Files.isDirectory(LOCATION)) { + return Stream.of(new LocationPreset("Dropbox", LOCATION)); + } else if(Files.isDirectory(FALLBACK_LOCATION)) { + return Stream.of(new LocationPreset("Dropbox", FALLBACK_LOCATION)); + } else { + return Stream.of(); + } + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/DropboxWindowsLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/DropboxWindowsLocationPresetsProvider.java new file mode 100644 index 000000000..4e0365275 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/DropboxWindowsLocationPresetsProvider.java @@ -0,0 +1,28 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +@CheckAvailability +public final class DropboxWindowsLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Dropbox"); + + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("Dropbox", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/GoogleDriveLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/GoogleDriveLocationPresetsProvider.java new file mode 100644 index 000000000..f54afe9af --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/GoogleDriveLocationPresetsProvider.java @@ -0,0 +1,37 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +@OperatingSystem(MAC) +@CheckAvailability +public final class GoogleDriveLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION1 = LocationPresetsProvider.resolveLocation("~/GoogleDrive"); + private static final Path LOCATION2 = LocationPresetsProvider.resolveLocation("~/GoogleDrive/My Drive"); + + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION1) || Files.isDirectory(LOCATION2); + } + + @Override + public Stream getLocations() { + if(Files.isDirectory(LOCATION1)) { + return Stream.of(new LocationPreset("Google Drive", LOCATION1)); + } else if(Files.isDirectory(LOCATION2)) { + return Stream.of(new LocationPreset("Google Drive", LOCATION2)); + } else { + return Stream.of(); + } + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/ICloudMacLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/ICloudMacLocationPresetsProvider.java new file mode 100644 index 000000000..9138610b5 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/ICloudMacLocationPresetsProvider.java @@ -0,0 +1,27 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; + +@OperatingSystem(MAC) +@CheckAvailability +public final class ICloudMacLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/Library/Mobile Documents/com~apple~CloudDocs"); + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("iCloud Drive", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/ICloudWindowsLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/ICloudWindowsLocationPresetsProvider.java new file mode 100644 index 000000000..f19333066 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/ICloudWindowsLocationPresetsProvider.java @@ -0,0 +1,27 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +@CheckAvailability +public final class ICloudWindowsLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/iCloudDrive"); + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("iCloud Drive", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/LocationPreset.java b/src/main/java/org/cryptomator/common/locationpresets/LocationPreset.java new file mode 100644 index 000000000..5ba49eb9b --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/LocationPreset.java @@ -0,0 +1,9 @@ +package org.cryptomator.common.locationpresets; + +import java.nio.file.Path; + +public record LocationPreset(String name, Path path) { + + + +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java new file mode 100644 index 000000000..7c499110b --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java @@ -0,0 +1,97 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.IntegrationsLoader; +import org.cryptomator.integrations.common.OperatingSystem; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.ServiceLoader; +import java.util.stream.Stream; + +public interface LocationPresetsProvider { + + Logger LOG = LoggerFactory.getLogger(LocationPresetsProvider.class); + String USER_HOME = System.getProperty("user.home"); + + /** + * Streams account-separated location presets found by this provider + * @return Stream of LocationPresets + */ + Stream getLocations(); + + static Path resolveLocation(String p) { + if (p.startsWith("~/")) { + return Path.of(USER_HOME, p.substring(2)); + } else { + return Path.of(p); + } + } + + //copied from org.cryptomator.integrations.common.IntegrationsLoader + //TODO: delete, once migrated to integrations-api + static Stream loadAll(Class clazz) { + return ServiceLoader.load(clazz) + .stream() + .filter(LocationPresetsProvider::isSupportedOperatingSystem) + .filter(LocationPresetsProvider::passesStaticAvailabilityCheck) + .map(ServiceLoader.Provider::get) + .peek(impl -> logServiceIsAvailable(clazz, impl.getClass())); + } + + + private static boolean isSupportedOperatingSystem(ServiceLoader.Provider provider) { + var annotations = provider.type().getAnnotationsByType(OperatingSystem.class); + return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent); + } + + private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider provider) { + return passesStaticAvailabilityCheck(provider.type()); + } + + static boolean passesStaticAvailabilityCheck(Class type) { + return passesAvailabilityCheck(type, null); + } + + private static void logServiceIsAvailable(Class apiType, Class implType) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName()); + } + } + + private static boolean passesAvailabilityCheck(Class type, @Nullable T instance) { + if (!type.isAnnotationPresent(CheckAvailability.class)) { + return true; // if type is not annotated, skip tests + } + if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) { + LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); + return false; + } + return Arrays.stream(type.getMethods()) + .filter(m -> isAvailabilityCheck(m, instance == null)) + .allMatch(m -> passesAvailabilityCheck(m, instance)); + } + + private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) { + assert Boolean.TYPE.equals(m.getReturnType()); + try { + return (boolean) m.invoke(instance); + } catch (ReflectiveOperationException e) { + LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e); + return false; + } + } + + private static boolean isAvailabilityCheck(Method m, boolean isStatic) { + return m.isAnnotationPresent(CheckAvailability.class) + && Boolean.TYPE.equals(m.getReturnType()) + && m.getParameterCount() == 0 + && Modifier.isStatic(m.getModifiers()) == isStatic; + } + +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/MegaLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/MegaLocationPresetsProvider.java new file mode 100644 index 000000000..f11b3d2bb --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/MegaLocationPresetsProvider.java @@ -0,0 +1,29 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +@OperatingSystem(MAC) +@CheckAvailability +public final class MegaLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/MEGA"); + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("MEGA", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/OneDriveLinuxLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/OneDriveLinuxLocationPresetsProvider.java new file mode 100644 index 000000000..7bcd1a6c3 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/OneDriveLinuxLocationPresetsProvider.java @@ -0,0 +1,28 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.LINUX; + +@OperatingSystem(LINUX) +@CheckAvailability +public final class OneDriveLinuxLocationPresetsProvider implements LocationPresetsProvider { + + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/OneDrive"); + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("OneDrive", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/OneDriveMacLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/OneDriveMacLocationPresetsProvider.java new file mode 100644 index 000000000..3657eac0d --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/OneDriveMacLocationPresetsProvider.java @@ -0,0 +1,44 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.OperatingSystem; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; + +@OperatingSystem(MAC) +public final class OneDriveMacLocationPresetsProvider implements LocationPresetsProvider { + + private static final Path FALLBACK_LOCATION = LocationPresetsProvider.resolveLocation("~/OneDrive"); + private static final Path PARENT_LOCATION = LocationPresetsProvider.resolveLocation("~/Library/CloudStorage"); + + @Override + public Stream getLocations() { + var newLocations = getNewLocations().toList(); + if (newLocations.size() >= 1) { + return newLocations.stream(); + } else { + return getOldLocation(); + } + } + + private Stream getNewLocations() { + try (var dirStream = Files.newDirectoryStream(PARENT_LOCATION, "OneDrive*")) { + return StreamSupport.stream(dirStream.spliterator(), false) // + .filter(Files::isDirectory) // + .map(p -> new LocationPreset(String.join(" - ", p.getFileName().toString().split("-")), p)); + } catch (IOException e) { + return Stream.of(); + } + } + + private Stream getOldLocation() { + return Stream.of(new LocationPreset("OneDrive", FALLBACK_LOCATION)).filter(preset -> Files.isDirectory(preset.path())); + } + + +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java new file mode 100644 index 000000000..7dcfde239 --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/OneDriveWindowsLocationPresetsProvider.java @@ -0,0 +1,108 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.OperatingSystem; +import org.jetbrains.annotations.Blocking; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +public final class OneDriveWindowsLocationPresetsProvider implements LocationPresetsProvider { + + private static final Logger LOG = LoggerFactory.getLogger(OneDriveWindowsLocationPresetsProvider.class); + private static final String REGSTR_TOKEN = "REG_SZ"; + private static final String REG_ONEDRIVE_ACCOUNTS = "HKEY_CURRENT_USER\\Software\\Microsoft\\OneDrive\\Accounts\\"; + + @Override + public Stream getLocations() { + try { + var accountRegKeys = queryRegistry(REG_ONEDRIVE_ACCOUNTS, List.of(), l -> l.startsWith(REG_ONEDRIVE_ACCOUNTS)).toList(); + var cloudLocations = new ArrayList(); + for (var accountRegKey : accountRegKeys) { + var path = queryRegistry(accountRegKey, List.of("/v", "UserFolder"), l -> l.contains("UserFolder")).map(result -> result.substring(result.indexOf(REGSTR_TOKEN) + REGSTR_TOKEN.length()).trim()) // + .map(Path::of) // + .findFirst().orElseThrow(); + var name = "OneDrive"; //we assume personal oneDrive account by default + if (!accountRegKey.endsWith("Personal")) { + name = queryRegistry(accountRegKey, List.of("/v", "DisplayName"), l -> l.contains("DisplayName")).map(result -> result.substring(result.indexOf(REGSTR_TOKEN) + REGSTR_TOKEN.length()).trim()) // + .map("OneDrive - "::concat) // + .findFirst().orElseThrow(); + } + cloudLocations.add(new LocationPreset(name, path)); + } + return cloudLocations.stream(); + } catch (IOException | CommandFailedException | TimeoutException e) { + LOG.error("Unable to determine OneDrive location", e); + return Stream.of(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("Determination of OneDrive location interrupted", e); + return Stream.of(); + } + } + + private Stream queryRegistry(String keyname, List moreArgs, Predicate outputFilter) throws InterruptedException, CommandFailedException, TimeoutException, IOException { + var args = new ArrayList(); + args.add("reg"); + args.add("query"); + args.add(keyname); + args.addAll(moreArgs); + ProcessBuilder command = new ProcessBuilder(args); + Process p = command.start(); + waitForSuccess(p, 3, "`reg query`"); + return p.inputReader(StandardCharsets.UTF_8).lines().filter(outputFilter); + } + + + /** + * Waits {@code timeoutSeconds} seconds for {@code process} to finish with exit code {@code 0}. + * + * @param process The process to wait for + * @param timeoutSeconds How long to wait (in seconds) + * @param cmdDescription A short description of the process used to generate log and exception messages + * @throws TimeoutException Thrown when the process doesn't finish in time + * @throws InterruptedException Thrown when the thread is interrupted while waiting for the process to finish + * @throws CommandFailedException Thrown when the process exit code is non-zero + */ + @Blocking + private static void waitForSuccess(Process process, int timeoutSeconds, String cmdDescription) throws TimeoutException, InterruptedException, CommandFailedException { + boolean exited = process.waitFor(timeoutSeconds, TimeUnit.SECONDS); + if (!exited) { + throw new TimeoutException(cmdDescription + " timed out after " + timeoutSeconds + "s"); + } + if (process.exitValue() != 0) { + @SuppressWarnings("resource") var stdout = process.inputReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n")); + @SuppressWarnings("resource") var stderr = process.errorReader(StandardCharsets.UTF_8).lines().collect(Collectors.joining("\n")); + throw new CommandFailedException(cmdDescription, process.exitValue(), stdout, stderr); + } + } + + private static class CommandFailedException extends Exception { + + int exitCode; + String stdout; + String stderr; + + private CommandFailedException(String cmdDescription, int exitCode, String stdout, String stderr) { + super(cmdDescription + " returned with non-zero exit code " + exitCode); + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + + } + + +} diff --git a/src/main/java/org/cryptomator/common/locationpresets/PCloudLocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/PCloudLocationPresetsProvider.java new file mode 100644 index 000000000..05e2867ce --- /dev/null +++ b/src/main/java/org/cryptomator/common/locationpresets/PCloudLocationPresetsProvider.java @@ -0,0 +1,30 @@ +package org.cryptomator.common.locationpresets; + +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.OperatingSystem; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.cryptomator.integrations.common.OperatingSystem.Value.MAC; +import static org.cryptomator.integrations.common.OperatingSystem.Value.WINDOWS; + +@OperatingSystem(WINDOWS) +@OperatingSystem(MAC) +@CheckAvailability +public final class PCloudLocationPresetsProvider implements LocationPresetsProvider { + + + private static final Path LOCATION = LocationPresetsProvider.resolveLocation("~/pCloudDrive"); + + @CheckAvailability + public static boolean isPresent() { + return Files.isDirectory(LOCATION); + } + + @Override + public Stream getLocations() { + return Stream.of(new LocationPreset("pCloud", LOCATION)); + } +} diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index cadccc091..25febe5e1 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -1,6 +1,9 @@ package org.cryptomator.ui.addvaultwizard; import dagger.Lazy; +import org.cryptomator.common.ObservableUtil; +import org.cryptomator.common.locationpresets.LocationPreset; +import org.cryptomator.common.locationpresets.LocationPresetsProvider; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; @@ -10,22 +13,19 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import java.io.File; @@ -34,6 +34,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; @AddVaultWizardScoped @@ -46,70 +49,72 @@ public class CreateNewVaultLocationController implements FxController { private final Stage window; private final Lazy chooseNameScene; private final Lazy choosePasswordScene; - private final ObservedLocationPresets locationPresets; + private final List locationPresetBtns; private final ObjectProperty vaultPath; private final StringProperty vaultName; private final ResourceBundle resourceBundle; - private final BooleanBinding validVaultPath; + private final ObservableValue vaultPathStatus; + private final ObservableValue validVaultPath; private final BooleanProperty usePresetPath; - private final StringProperty statusText; - private final ObjectProperty statusGraphic; private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; //FXML - public ToggleGroup predefinedLocationToggler; - public RadioButton iclouddriveRadioButton; - public RadioButton dropboxRadioButton; - public RadioButton gdriveRadioButton; - public RadioButton onedriveRadioButton; - public RadioButton megaRadioButton; - public RadioButton pcloudRadioButton; + public ToggleGroup locationPresetsToggler; + public VBox radioButtonVBox; public RadioButton customRadioButton; - public Label vaultPathStatus; + public Label locationStatusLabel; public FontAwesome5IconView goodLocation; public FontAwesome5IconView badLocation; @Inject - CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { + CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, ObjectProperty vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { this.window = window; this.chooseNameScene = chooseNameScene; this.choosePasswordScene = choosePasswordScene; - this.locationPresets = locationPresets; this.vaultPath = vaultPath; this.vaultName = vaultName; this.resourceBundle = resourceBundle; - this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath); + this.vaultPathStatus = ObservableUtil.mapWithDefault(vaultPath, this::validatePath, new VaultPathStatus(false, "error.message")); + this.validVaultPath = ObservableUtil.mapWithDefault(vaultPathStatus, VaultPathStatus::valid, false); + this.vaultPathStatus.addListener(this::updateStatusLabel); this.usePresetPath = new SimpleBooleanProperty(); - this.statusText = new SimpleStringProperty(); - this.statusGraphic = new SimpleObjectProperty<>(); + this.locationPresetBtns = LocationPresetsProvider.loadAll(LocationPresetsProvider.class) // + .flatMap(LocationPresetsProvider::getLocations) // + .sorted(Comparator.comparing(LocationPreset::name)) // + .map(preset -> { // + var btn = new RadioButton(preset.name()); + btn.setUserData(preset.path()); + return btn; + }).toList(); } - private boolean validateVaultPathAndSetStatus() { - final Path p = vaultPath.get(); - if (p == null) { - statusText.set("Error: Path is NULL."); - statusGraphic.set(badLocation); - return false; - } else if (!Files.exists(p.getParent())) { - statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist")); - statusGraphic.set(badLocation); - return false; + private VaultPathStatus validatePath(Path p) throws NullPointerException { + if (!Files.exists(p.getParent())) { + return new VaultPathStatus(false, "addvaultwizard.new.locationDoesNotExist"); } else if (!isActuallyWritable(p.getParent())) { - statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable")); - statusGraphic.set(badLocation); - return false; + return new VaultPathStatus(false, "addvaultwizard.new.locationIsNotWritable"); } else if (!Files.notExists(p)) { - statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists")); - statusGraphic.set(badLocation); - return false; + return new VaultPathStatus(false, "addvaultwizard.new.fileAlreadyExists"); } else { - statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk")); - statusGraphic.set(goodLocation); - return true; + return new VaultPathStatus(true, "addvaultwizard.new.locationIsOk"); } } + private void updateStatusLabel(ObservableValue observable, VaultPathStatus oldValue, VaultPathStatus newValue) { + if (newValue.valid()) { + locationStatusLabel.setGraphic(goodLocation); + locationStatusLabel.getStyleClass().remove("label-red"); + locationStatusLabel.getStyleClass().add("label-muted"); + } else { + locationStatusLabel.setGraphic(badLocation); + locationStatusLabel.getStyleClass().remove("label-muted"); + locationStatusLabel.getStyleClass().add("label-red"); + } + this.locationStatusLabel.setText(resourceBundle.getString(newValue.localizationKey())); + } + + private boolean isActuallyWritable(Path p) { Path tmpFile = p.resolve(TEMP_FILE_FORMAT); try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) { @@ -127,26 +132,15 @@ public class CreateNewVaultLocationController implements FxController { @FXML public void initialize() { - predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); - usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); + radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header + locationPresetsToggler.getToggles().addAll(locationPresetBtns); + locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); + usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); } private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { - if (iclouddriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get())); - } else if (dropboxRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get())); - } else if (gdriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); - } else if (onedriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get())); - } else if (megaRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get())); - } else if (pcloudRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get())); - } else if (customRadioButton.equals(newValue)) { - vaultPath.set(customVaultPath.resolve(vaultName.get())); - } + var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath); + vaultPath.set(storagePath.resolve(vaultName.get())); } @FXML @@ -156,10 +150,8 @@ public class CreateNewVaultLocationController implements FxController { @FXML public void next() { - if (validateVaultPathAndSetStatus()) { + if (validVaultPath.getValue()) { window.setScene(choosePasswordScene.get()); - } else { - validVaultPath.invalidate(); } } @@ -179,6 +171,12 @@ public class CreateNewVaultLocationController implements FxController { } } + /* Internal classes */ + + private record VaultPathStatus(boolean valid, String localizationKey) { + + } + /* Getter/Setter */ public Path getVaultPath() { @@ -189,47 +187,28 @@ public class CreateNewVaultLocationController implements FxController { return vaultPath; } - public BooleanBinding validVaultPathProperty() { + public ObservableValue validVaultPathProperty() { return validVaultPath; } - public Boolean getValidVaultPath() { - return validVaultPath.get(); - } - - public ObservedLocationPresets getObservedLocationPresets() { - return locationPresets; + public boolean isValidVaultPath() { + return validVaultPath.getValue(); } public BooleanProperty usePresetPathProperty() { return usePresetPath; } - public boolean getUsePresetPath() { + public boolean isUsePresetPath() { return usePresetPath.get(); } public BooleanBinding anyRadioButtonSelectedProperty() { - return predefinedLocationToggler.selectedToggleProperty().isNotNull(); + return locationPresetsToggler.selectedToggleProperty().isNotNull(); } public boolean isAnyRadioButtonSelected() { return anyRadioButtonSelectedProperty().get(); } - public StringProperty statusTextProperty() { - return statusText; - } - - public String getStatusText() { - return statusText.get(); - } - - public ObjectProperty statusGraphicProperty() { - return statusGraphic; - } - - public Node getStatusGraphic() { - return statusGraphic.get(); - } } diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java deleted file mode 100644 index 1c988dc04..000000000 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.cryptomator.ui.addvaultwizard; - -import org.cryptomator.common.LocationPreset; - -import javax.inject.Inject; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import java.nio.file.Path; - -@AddVaultWizardScoped -public class ObservedLocationPresets { - - private final ReadOnlyObjectProperty iclouddriveLocation; - private final ReadOnlyObjectProperty dropboxLocation; - private final ReadOnlyObjectProperty gdriveLocation; - private final ReadOnlyObjectProperty onedriveLocation; - private final ReadOnlyObjectProperty megaLocation; - private final ReadOnlyObjectProperty pcloudLocation; - private final BooleanBinding foundIclouddrive; - private final BooleanBinding foundDropbox; - private final BooleanBinding foundGdrive; - private final BooleanBinding foundOnedrive; - private final BooleanBinding foundMega; - private final BooleanBinding foundPcloud; - - @Inject - public ObservedLocationPresets() { - this.iclouddriveLocation = new SimpleObjectProperty<>(LocationPreset.ICLOUDDRIVE.existingPath()); - this.dropboxLocation = new SimpleObjectProperty<>(LocationPreset.DROPBOX.existingPath()); - this.gdriveLocation = new SimpleObjectProperty<>(LocationPreset.GDRIVE.existingPath()); - this.onedriveLocation = new SimpleObjectProperty<>(LocationPreset.ONEDRIVE.existingPath()); - this.megaLocation = new SimpleObjectProperty<>(LocationPreset.MEGA.existingPath()); - this.pcloudLocation = new SimpleObjectProperty<>(LocationPreset.PCLOUD.existingPath()); - this.foundIclouddrive = iclouddriveLocation.isNotNull(); - this.foundDropbox = dropboxLocation.isNotNull(); - this.foundGdrive = gdriveLocation.isNotNull(); - this.foundOnedrive = onedriveLocation.isNotNull(); - this.foundMega = megaLocation.isNotNull(); - this.foundPcloud = pcloudLocation.isNotNull(); - } - - /* Observables */ - - public ReadOnlyObjectProperty iclouddriveLocationProperty() { - return iclouddriveLocation; - } - - public Path getIclouddriveLocation() { - return iclouddriveLocation.get(); - } - - public BooleanBinding foundIclouddriveProperty() { - return foundIclouddrive; - } - - public boolean isFoundIclouddrive() { - return foundIclouddrive.get(); - } - - public ReadOnlyObjectProperty dropboxLocationProperty() { - return dropboxLocation; - } - - public Path getDropboxLocation() { - return dropboxLocation.get(); - } - - public BooleanBinding foundDropboxProperty() { - return foundDropbox; - } - - public boolean isFoundDropbox() { - return foundDropbox.get(); - } - - public ReadOnlyObjectProperty gdriveLocationProperty() { - return gdriveLocation; - } - - public Path getGdriveLocation() { - return gdriveLocation.get(); - } - - public BooleanBinding foundGdriveProperty() { - return foundGdrive; - } - - public boolean isFoundGdrive() { - return foundGdrive.get(); - } - - public ReadOnlyObjectProperty onedriveLocationProperty() { - return onedriveLocation; - } - - public Path getOnedriveLocation() { - return onedriveLocation.get(); - } - - public BooleanBinding foundOnedriveProperty() { - return foundOnedrive; - } - - public boolean isFoundOnedrive() { - return foundOnedrive.get(); - } - - public ReadOnlyObjectProperty megaLocationProperty() { - return megaLocation; - } - - public Path getMegaLocation() { - return megaLocation.get(); - } - - public BooleanBinding foundMegaProperty() { - return foundMega; - } - - public boolean isFoundMega() { - return foundMega.get(); - } - - public ReadOnlyObjectProperty pcloudLocationProperty() { - return pcloudLocation; - } - - public Path getPcloudLocation() { - return pcloudLocation.get(); - } - - public BooleanBinding foundPcloudProperty() { - return foundPcloud; - } - - public boolean isFoundPcloud() { - return foundPcloud.get(); - } - -} diff --git a/src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java b/src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java index 973d919fc..e99cd6680 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/AutoUnlocker.java @@ -1,30 +1,85 @@ package org.cryptomator.ui.fxapp; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultListManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.collections.ObservableList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Stream; @FxApplicationScoped public class AutoUnlocker { + private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class); + private final ObservableList vaults; private final FxApplicationWindows appWindows; + private final ScheduledExecutorService scheduler; + private ScheduledFuture unlockMissingFuture; + private ScheduledFuture timeoutFuture; @Inject - public AutoUnlocker(ObservableList vaults, FxApplicationWindows appWindows) { + public AutoUnlocker(ObservableList vaults, FxApplicationWindows appWindows, ScheduledExecutorService scheduler) { this.vaults = vaults; this.appWindows = appWindows; + this.scheduler = scheduler; } - public void unlock() { - vaults.stream().filter(Vault::isLocked) // - .filter(v -> v.getVaultSettings().unlockAfterStartup().get()) // - .>reduce(CompletableFuture.completedFuture(null), // - (unlockFlow, v) -> unlockFlow.handle((voit, ex) -> appWindows.startUnlockWorkflow(v, null)).thenCompose(stage -> stage), //we don't care here about the exception, logged elsewhere - (unlockChain1, unlockChain2) -> unlockChain1.handle((voit, ex) -> unlockChain2).thenCompose(stage -> stage)); + public void tryUnlockForTimespan(int timespan, TimeUnit timeUnit) { + // Unlock all available auto unlock vaults + Predicate shouldAutoUnlock = v -> v.getVaultSettings().unlockAfterStartup().get(); + unlockSequentially(vaults.stream().filter(shouldAutoUnlock)).thenRun(() -> startUnlockMissing(timespan, timeUnit)); } + private CompletionStage unlockSequentially(Stream vaultStream) { + // this is an attempt to run all the unlock workflows sequentially, i.e. start the next workflow only after completing/failing the previous workflow. + return vaultStream.filter(Vault::isLocked).reduce(CompletableFuture.completedFuture(null), + (prevUnlock, nextVault) -> prevUnlock.thenCompose(unused -> appWindows.startUnlockWorkflow(nextVault, null)), + (prevUnlock, nextUnlock) -> nextUnlock.exceptionally(e -> null) // we don't care here about the exception, logged elsewhere + ); + } + + private void startUnlockMissing(int timespan, TimeUnit timeUnit) { + // Start a temporary service if there are missing auto unlock vaults + if (getMissingAutoUnlockVaults().findAny().isPresent()) { + LOG.info("Found MISSING vaults, starting periodic check"); + unlockMissingFuture = scheduler.scheduleWithFixedDelay(this::unlockMissing, 0, 1, TimeUnit.SECONDS); + timeoutFuture = scheduler.schedule(this::timeout, timespan, timeUnit); + } + } + + private void unlockMissing() { + List missingAutoUnlockVaults = getMissingAutoUnlockVaults().toList(); + missingAutoUnlockVaults.forEach(VaultListManager::redetermineVaultState); + unlockSequentially(missingAutoUnlockVaults.stream()).thenRun(this::stopUnlockMissing); + } + + private void stopUnlockMissing() { + // Stop checking if there are no more missing vaults + if (getMissingAutoUnlockVaults().findAny().isEmpty()) { + LOG.info("No more MISSING vaults, stopping periodic check"); + unlockMissingFuture.cancel(false); + timeoutFuture.cancel(false); + } + } + + private void timeout() { + LOG.info("MISSING vaults periodic check timed out"); + unlockMissingFuture.cancel(false); + } + + private Stream getMissingAutoUnlockVaults() { + return vaults.stream() + .filter(Vault::isMissing) + .filter(v -> v.getVaultSettings().unlockAfterStartup().get()); + } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 3ddb7cba6..cfc7ba62b 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; +import java.util.concurrent.TimeUnit; @FxApplicationScoped public class FxApplication { @@ -68,7 +69,6 @@ public class FxApplication { }); launchEventHandler.startHandlingLaunchEvents(); - autoUnlocker.unlock(); + autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES); } - } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java index 2b4f8e7bc..000954bd5 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java @@ -12,6 +12,7 @@ import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.preferences.SelectedPreferencesTab; import org.cryptomator.ui.quit.QuitComponent; import org.cryptomator.ui.unlock.UnlockComponent; +import org.cryptomator.ui.unlock.UnlockWorkflow; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,7 +115,7 @@ public class FxApplicationWindows { LOG.debug("Start unlock workflow for {}", vault.getDisplayName()); return unlockWorkflowFactory.create(vault, owner).unlockWorkflow(); }, Platform::runLater) // - .thenCompose(unlockWorkflow -> CompletableFuture.runAsync(unlockWorkflow, executor)) // + .thenAcceptAsync(UnlockWorkflow::run, executor) .exceptionally(e -> { showErrorWindow(e, owner == null ? primaryStage : owner, null); return null; diff --git a/src/main/resources/fxml/addvault_new_location.fxml b/src/main/resources/fxml/addvault_new_location.fxml index 343a2d580..ffdc7fa4e 100644 --- a/src/main/resources/fxml/addvault_new_location.fxml +++ b/src/main/resources/fxml/addvault_new_location.fxml @@ -19,7 +19,7 @@ spacing="12" alignment="CENTER_LEFT"> - + @@ -29,16 +29,11 @@ - +