mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 18:51:26 +00:00
Merge branch 'develop' of https://github.com/cryptomator/cryptomator into feature/error-dialog
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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", "~/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<Path> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> PATTERN = Pattern.compile("Dropbox \\(.+\\)").asMatchPredicate();
|
||||
|
||||
@Override
|
||||
public Stream<LocationPreset> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("Dropbox", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("iCloud Drive", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("iCloud Drive", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.cryptomator.common.locationpresets;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record LocationPreset(String name, Path path) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<LocationPreset> 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 <T> Stream<T> loadAll(Class<T> 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 <T> boolean passesAvailabilityCheck(Class<? extends T> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("MEGA", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("OneDrive", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
var newLocations = getNewLocations().toList();
|
||||
if (newLocations.size() >= 1) {
|
||||
return newLocations.stream();
|
||||
} else {
|
||||
return getOldLocation();
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<LocationPreset> 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<LocationPreset> getOldLocation() {
|
||||
return Stream.of(new LocationPreset("OneDrive", FALLBACK_LOCATION)).filter(preset -> Files.isDirectory(preset.path()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
try {
|
||||
var accountRegKeys = queryRegistry(REG_ONEDRIVE_ACCOUNTS, List.of(), l -> l.startsWith(REG_ONEDRIVE_ACCOUNTS)).toList();
|
||||
var cloudLocations = new ArrayList<LocationPreset>();
|
||||
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<String> queryRegistry(String keyname, List<String> moreArgs, Predicate<String> outputFilter) throws InterruptedException, CommandFailedException, TimeoutException, IOException {
|
||||
var args = new ArrayList<String>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<LocationPreset> getLocations() {
|
||||
return Stream.of(new LocationPreset("pCloud", LOCATION));
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.function.Function;
|
||||
|
||||
// TODO make sealed, remove enum
|
||||
interface IpcMessage {
|
||||
//TODO can the enum be removed?
|
||||
sealed interface IpcMessage permits HandleLaunchArgsMessage, RevealRunningAppMessage {
|
||||
|
||||
enum MessageType {
|
||||
REVEAL_RUNNING_APP(RevealRunningAppMessage::decode),
|
||||
|
||||
@@ -5,10 +5,9 @@ import java.util.List;
|
||||
public interface IpcMessageListener {
|
||||
|
||||
default void handleMessage(IpcMessage message) {
|
||||
if (message instanceof RevealRunningAppMessage) {
|
||||
revealRunningApp();
|
||||
} else if (message instanceof HandleLaunchArgsMessage m) {
|
||||
handleLaunchArgs(m.args());
|
||||
switch (message) {
|
||||
case RevealRunningAppMessage m -> revealRunningApp(); // TODO: rename to _ with JEP 443
|
||||
case HandleLaunchArgsMessage m -> handleLaunchArgs(m.args());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Singleton
|
||||
public class SupportedLanguages {
|
||||
@@ -18,9 +18,9 @@ public class SupportedLanguages {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
|
||||
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_".
|
||||
// "en" is not part of this list - it is always inserted at the top.
|
||||
public static final List<String> LANGUAGE_TAGS = List.of("ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fil", "fa", "fr", "gl", "he", //
|
||||
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "si", "sk", "sr", "sr-Latn", "sv", "sw", //
|
||||
"ta", "te", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
|
||||
public static final List<String> LANGUAGE_TAGS = List.of("ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fr", "gl", "he", //
|
||||
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "nb", "nl", "nn", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", "sr-Latn", "sv", "sw", //
|
||||
"ta", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
|
||||
public static final String ENGLISH = "en";
|
||||
|
||||
private final List<String> sortedLanguageTags;
|
||||
@@ -33,11 +33,12 @@ public class SupportedLanguages {
|
||||
preferredLocale = preferredLanguage == null ? Locale.getDefault() : Locale.forLanguageTag(preferredLanguage);
|
||||
var collator = Collator.getInstance(preferredLocale);
|
||||
collator.setStrength(Collator.PRIMARY);
|
||||
var sorted = LANGUAGE_TAGS.stream() //
|
||||
.sorted((a, b) -> collator.compare(Locale.forLanguageTag(a).getDisplayName(), Locale.forLanguageTag(b).getDisplayName())) //
|
||||
.collect(Collectors.toList());
|
||||
var sorted = new ArrayList<String>();
|
||||
sorted.add(0, Settings.DEFAULT_LANGUAGE);
|
||||
sorted.add(1, ENGLISH);
|
||||
LANGUAGE_TAGS.stream() //
|
||||
.sorted((a, b) -> collator.compare(Locale.forLanguageTag(a).getDisplayName(), Locale.forLanguageTag(b).getDisplayName())) //
|
||||
.forEach(sorted::add);
|
||||
sortedLanguageTags = Collections.unmodifiableList(sorted);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Scene> chooseNameScene;
|
||||
private final Lazy<Scene> choosePasswordScene;
|
||||
private final ObservedLocationPresets locationPresets;
|
||||
private final List<RadioButton> locationPresetBtns;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final BooleanBinding validVaultPath;
|
||||
private final ObservableValue<VaultPathStatus> vaultPathStatus;
|
||||
private final ObservableValue<Boolean> validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final StringProperty statusText;
|
||||
private final ObjectProperty<Node> 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<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObjectProperty<Path> 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<? extends VaultPathStatus> 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<? extends Toggle> 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<Boolean> 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<Node> statusGraphicProperty() {
|
||||
return statusGraphic;
|
||||
}
|
||||
|
||||
public Node getStatusGraphic() {
|
||||
return statusGraphic.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Path> iclouddriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> megaLocation;
|
||||
private final ReadOnlyObjectProperty<Path> 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<Path> iclouddriveLocationProperty() {
|
||||
return iclouddriveLocation;
|
||||
}
|
||||
|
||||
public Path getIclouddriveLocation() {
|
||||
return iclouddriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundIclouddriveProperty() {
|
||||
return foundIclouddrive;
|
||||
}
|
||||
|
||||
public boolean isFoundIclouddrive() {
|
||||
return foundIclouddrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
|
||||
return dropboxLocation;
|
||||
}
|
||||
|
||||
public Path getDropboxLocation() {
|
||||
return dropboxLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundDropboxProperty() {
|
||||
return foundDropbox;
|
||||
}
|
||||
|
||||
public boolean isFoundDropbox() {
|
||||
return foundDropbox.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
|
||||
return gdriveLocation;
|
||||
}
|
||||
|
||||
public Path getGdriveLocation() {
|
||||
return gdriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundGdriveProperty() {
|
||||
return foundGdrive;
|
||||
}
|
||||
|
||||
public boolean isFoundGdrive() {
|
||||
return foundGdrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
|
||||
return onedriveLocation;
|
||||
}
|
||||
|
||||
public Path getOnedriveLocation() {
|
||||
return onedriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundOnedriveProperty() {
|
||||
return foundOnedrive;
|
||||
}
|
||||
|
||||
public boolean isFoundOnedrive() {
|
||||
return foundOnedrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
|
||||
return megaLocation;
|
||||
}
|
||||
|
||||
public Path getMegaLocation() {
|
||||
return megaLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundMegaProperty() {
|
||||
return foundMega;
|
||||
}
|
||||
|
||||
public boolean isFoundMega() {
|
||||
return foundMega.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
|
||||
return pcloudLocation;
|
||||
}
|
||||
|
||||
public Path getPcloudLocation() {
|
||||
return pcloudLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundPcloudProperty() {
|
||||
return foundPcloud;
|
||||
}
|
||||
|
||||
public boolean isFoundPcloud() {
|
||||
return foundPcloud.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Vault> vaults;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private ScheduledFuture<?> unlockMissingFuture;
|
||||
private ScheduledFuture<?> timeoutFuture;
|
||||
|
||||
@Inject
|
||||
public AutoUnlocker(ObservableList<Vault> vaults, FxApplicationWindows appWindows) {
|
||||
public AutoUnlocker(ObservableList<Vault> 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()) //
|
||||
.<CompletionStage<Void>>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<Vault> shouldAutoUnlock = v -> v.getVaultSettings().unlockAfterStartup().get();
|
||||
unlockSequentially(vaults.stream().filter(shouldAutoUnlock)).thenRun(() -> startUnlockMissing(timespan, timeUnit));
|
||||
}
|
||||
|
||||
private CompletionStage<Void> unlockSequentially(Stream<Vault> 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<Vault> 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<Vault> getMissingAutoUnlockVaults() {
|
||||
return vaults.stream()
|
||||
.filter(Vault::isMissing)
|
||||
.filter(v -> v.getVaultSettings().unlockAfterStartup().get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -102,14 +102,16 @@ public class StartController implements FxController {
|
||||
}
|
||||
|
||||
private void loadingKeyFailed(Throwable e) {
|
||||
if (e instanceof UnlockCancelledException) {
|
||||
// ok
|
||||
} else if (e instanceof VaultKeyInvalidException) {
|
||||
LOG.error("Invalid key"); //TODO: specific error screen
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
} else {
|
||||
LOG.error("Failed to load key.", e);
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
switch (e) {
|
||||
case UnlockCancelledException uce -> {} //ok
|
||||
case VaultKeyInvalidException vkie -> {
|
||||
LOG.error("Invalid key"); //TODO: specific error screen
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
}
|
||||
default -> {
|
||||
LOG.error("Failed to load key.", e);
|
||||
appWindows.showErrorWindow(e, window, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,18 +84,19 @@ public class AwtTrayMenuController implements TrayMenuController {
|
||||
|
||||
private void addChildren(Menu menu, List<TrayMenuItem> items) {
|
||||
for (var item : items) {
|
||||
// TODO: use Pattern Matching for switch, once available
|
||||
if (item instanceof ActionItem a) {
|
||||
var menuItem = new MenuItem(a.title());
|
||||
menuItem.addActionListener(evt -> a.action().run());
|
||||
menuItem.setEnabled(a.enabled());
|
||||
menu.add(menuItem);
|
||||
} else if (item instanceof SeparatorItem) {
|
||||
menu.addSeparator();
|
||||
} else if (item instanceof SubMenuItem s) {
|
||||
var submenu = new Menu(s.title());
|
||||
addChildren(submenu, s.items());
|
||||
menu.add(submenu);
|
||||
switch (item) {
|
||||
case ActionItem a -> {
|
||||
var menuItem = new MenuItem(a.title());
|
||||
menuItem.addActionListener(evt -> a.action().run());
|
||||
menuItem.setEnabled(a.enabled());
|
||||
menu.add(menuItem);
|
||||
}
|
||||
case SeparatorItem s -> menu.addSeparator(); // TODO: rename to _ with JEP 443
|
||||
case SubMenuItem s -> {
|
||||
var submenu = new Menu(s.title());
|
||||
addChildren(submenu, s.items());
|
||||
menu.add(submenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,11 @@ public class UnlockInvalidMountPointController implements FxController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
var e = unlockException.get();
|
||||
String translationKey = "unlock.error.customPath.description.generic";
|
||||
if (e instanceof MountPointNotSupportedException) {
|
||||
translationKey = "unlock.error.customPath.description.notSupported";
|
||||
} else if (e instanceof MountPointNotExistsException) {
|
||||
translationKey = "unlock.error.customPath.description.notExists";
|
||||
}
|
||||
var translationKey = switch (e) {
|
||||
case MountPointNotSupportedException x -> "unlock.error.customPath.description.notSupported";
|
||||
case MountPointNotExistsException x -> "unlock.error.customPath.description.notExists";
|
||||
default -> "unlock.error.customPath.description.generic";
|
||||
};
|
||||
dialogDescription.setFormat(resourceBundle.getString(translationKey));
|
||||
dialogDescription.setArg1(e.getMessage());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user