diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dca01d819..a3e9a2b59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,7 +97,7 @@ jobs: echo "::set-output name=semVerNum::${SEM_VER_NUM}" echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" echo "::set-output name=revNum::${REVCOUNT}" - - uses: rubenesp87/semver-validation-action@0.0.6 + - uses: skymatic/semver-validation-action@v1 with: version: ${{ steps.versions.outputs.semVerStr }} @@ -283,7 +283,7 @@ jobs: run: | mv appdir/Cryptomator Cryptomator.AppDir cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ - envsubst '${REVISION_NO}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh + envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon @@ -291,6 +291,7 @@ jobs: ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun env: REVISION_NO: ${{ needs.metadata.outputs.revNum }} + SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 run: | JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` diff --git a/dist/linux/appimage/resources/AppDir/bin/cryptomator.sh b/dist/linux/appimage/resources/AppDir/bin/cryptomator.sh index fa86a2c30..06cd8f110 100755 --- a/dist/linux/appimage/resources/AppDir/bin/cryptomator.sh +++ b/dist/linux/appimage/resources/AppDir/bin/cryptomator.sh @@ -26,12 +26,15 @@ export LD_PRELOAD=lib/app/libjffi.so ./lib/runtime/bin/java \ -p "lib/app/mods" \ -cp "lib/app/*" \ + -Dfile.encoding="utf-8" \ -Dcryptomator.logDir="~/.local/share/Cryptomator/logs" \ + -Dcryptomator.pluginDir="~/.local/share/Cryptomator/plugins" -Dcryptomator.mountPointsDir="~/.local/share/Cryptomator/mnt" \ -Dcryptomator.settingsPath="~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json" \ -Dcryptomator.ipcSocketPath="~/.config/Cryptomator/ipc.socket" \ -Dcryptomator.buildNumber="appimage-${REVISION_NO}" \ + -Dcryptomator.appVersion="${SEMVER_STR}" \ $GTK_FLAG \ - -Xss2m \ - -Xmx512m \ + -Xss5m \ + -Xmx256m \ -m org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator diff --git a/pom.xml b/pom.xml index efb61f369..4b6ad5119 100644 --- a/pom.xml +++ b/pom.xml @@ -27,33 +27,41 @@ com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh - 2.1.0-beta9 - 1.0.0-rc1 - 1.0.0-beta2 - 1.0.0-beta2 - 1.0.0-beta1 - 1.3.1 - 1.3.2 + 2.1.0-beta13 + 1.0.0 + 1.0.0-rc1 + 1.0.0-rc1 + 1.0.0-rc1 + 1.3.2 + 1.3.3 1.2.6 - 16 + 17.0.0.1 3.12.0 - 3.18.1 + 3.18.2 2.2 - 30.1.1-jre - 2.37 - 2.8.7 + 31.0-jre + 2.38.1 + 2.8.8 1.5.2 - 1.7.31 - 1.2.3 + 1.7.32 + 1.2.6 - 5.7.2 - 3.11.2 + 5.8.1 + 3.12.4 2.2 + + + google-maven + Google Maven Repo + https://maven.google.com + + + @@ -263,7 +271,7 @@ org.owasp dependency-check-maven - 6.2.2 + 6.3.1 diff --git a/src/main/java/org/cryptomator/common/Constants.java b/src/main/java/org/cryptomator/common/Constants.java index 06dedfc2d..90bd3c8ec 100644 --- a/src/main/java/org/cryptomator/common/Constants.java +++ b/src/main/java/org/cryptomator/common/Constants.java @@ -5,6 +5,7 @@ public interface Constants { String MASTERKEY_FILENAME = "masterkey.cryptomator"; String MASTERKEY_BACKUP_SUFFIX = ".bkup"; String VAULTCONFIG_FILENAME = "vault.cryptomator"; + String CRYPTOMATOR_FILENAME_EXT = ".cryptomator"; byte[] PEPPER = new byte[0]; } diff --git a/src/main/java/org/cryptomator/common/ErrorCode.java b/src/main/java/org/cryptomator/common/ErrorCode.java new file mode 100644 index 000000000..51fb355b6 --- /dev/null +++ b/src/main/java/org/cryptomator/common/ErrorCode.java @@ -0,0 +1,121 @@ +package org.cryptomator.common; + +import com.google.common.base.Strings; +import com.google.common.base.Throwables; + +import java.util.Locale; +import java.util.Objects; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Holds a throwable and provides a human-readable {@link #toString() three-component string representation} + * aiming to allow documentation and lookup of same or similar errors. + */ +public class ErrorCode { + + private final static int A_PRIME = 31; + private final static int SEED = 0xdeadbeef; + public final static String DELIM = ":"; + + private final static int LATEST_FRAME = 1; + private final static int ALL_FRAMES = Integer.MAX_VALUE; + + private final Throwable throwable; + private final Throwable rootCause; + private final int rootCauseSpecificFrames; + + private ErrorCode(Throwable throwable, Throwable rootCause, int rootCauseSpecificFrames) { + this.throwable = Objects.requireNonNull(throwable); + this.rootCause = Objects.requireNonNull(rootCause); + this.rootCauseSpecificFrames = rootCauseSpecificFrames; + } + + // visible for testing + String methodCode() { + return format(traceCode(rootCause, LATEST_FRAME)); + } + + // visible for testing + String rootCauseCode() { + return format(traceCode(rootCause, rootCauseSpecificFrames)); + } + + // visible for testing + String throwableCode() { + return format(traceCode(throwable, ALL_FRAMES)); + } + + /** + * Produces an error code consisting of three {@value DELIM}-separated components. + *

+ * A full match of the error code indicates the exact same throwable (to the extent possible + * without hash collisions). A partial match of the first or second component indicates related problems + * with the same root cause. + * + * @return A three-part error code + */ + @Override + public String toString() { + return methodCode() + DELIM + rootCauseCode() + DELIM + throwableCode(); + } + + /** + * Deterministically creates an error code from the stack trace of the given cause. + *

+ * The code consists of three parts separated by {@value DELIM}: + *

+ *

+ * Parts may be identical if the cause is the root cause or the root cause has just one single item in its stack trace. + * + * @param throwable The exception + * @return A three-part error code + */ + public static ErrorCode of(Throwable throwable) { + var causalChain = Throwables.getCausalChain(throwable); + if (causalChain.size() > 1) { + var rootCause = causalChain.get(causalChain.size() - 1); + var parentOfRootCause = causalChain.get(causalChain.size() - 2); + var rootSpecificFrames = nonOverlappingFrames(parentOfRootCause.getStackTrace(), rootCause.getStackTrace()); + return new ErrorCode(throwable, rootCause, rootSpecificFrames); + } else { + return new ErrorCode(throwable, throwable, ALL_FRAMES); + } + } + + private String format(int value) { + // Cut off highest 12 bits (only leave 20 least significant bits) and XOR rest with cutoff + value = (value & 0xfffff) ^ (value >>> 20); + return Strings.padStart(Integer.toString(value, 32).toUpperCase(Locale.ROOT), 4, '0'); + } + + private int traceCode(Throwable e, int frameCount) { + int result = SEED; + if (e.getCause() != null) { + result = traceCode(e.getCause(), frameCount); + } + result = result * A_PRIME + e.getClass().getName().hashCode(); + var stack = e.getStackTrace(); + for (int i = 0; i < Math.min(stack.length, frameCount); i++) { + result = result * A_PRIME + stack[i].getClassName().hashCode(); + result = result * A_PRIME + stack[i].getMethodName().hashCode(); + } + return result; + } + + private static int nonOverlappingFrames(StackTraceElement[] frames, StackTraceElement[] enclosingFrames) { + // Compute the number of elements in `frames` not contained in `enclosingFrames` by iterating backwards + // Result should usually be equal to the difference in size of both traces + var i = reverseStream(enclosingFrames).iterator(); + return (int) reverseStream(frames).dropWhile(f -> i.hasNext() && i.next().equals(f)).count(); + } + + private static Stream reverseStream(T[] array) { + return IntStream.rangeClosed(1, array.length).mapToObj(i -> array[array.length - i]); + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java index 57af6c685..c97d0e9c9 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java @@ -38,6 +38,11 @@ public class KeychainManager implements KeychainAccessProvider { return result; } + @Override + public String displayName() { + return getClass().getName(); + } + @Override public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { getKeychainOrFail().storePassphrase(key, passphrase); diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index e4cb9b8f7..82f2fb794 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/src/main/java/org/cryptomator/common/settings/Settings.java @@ -43,6 +43,8 @@ public class Settings { public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT; public static final String DEFAULT_LICENSE_KEY = ""; public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false; + public static final String DEFAULT_DISPLAY_CONFIGURATION = ""; + private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables); private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK); @@ -59,6 +61,12 @@ public class Settings { private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY); private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON); private final BooleanProperty showTrayIcon; + private final IntegerProperty windowXPosition = new SimpleIntegerProperty(); + private final IntegerProperty windowYPosition = new SimpleIntegerProperty(); + private final IntegerProperty windowWidth = new SimpleIntegerProperty(); + private final IntegerProperty windowHeight = new SimpleIntegerProperty(); + private final ObjectProperty displayConfiguration = new SimpleObjectProperty<>(DEFAULT_DISPLAY_CONFIGURATION); + private Consumer saveCmd; @@ -83,6 +91,11 @@ public class Settings { licenseKey.addListener(this::somethingChanged); showMinimizeButton.addListener(this::somethingChanged); showTrayIcon.addListener(this::somethingChanged); + windowXPosition.addListener(this::somethingChanged); + windowYPosition.addListener(this::somethingChanged); + windowWidth.addListener(this::somethingChanged); + windowHeight.addListener(this::somethingChanged); + displayConfiguration.addListener(this::somethingChanged); } void setSaveCmd(Consumer saveCmd) { @@ -141,7 +154,7 @@ public class Settings { return theme; } - public ObjectProperty keychainProvider() { return keychainProvider; } + public ObjectProperty keychainProvider() {return keychainProvider;} public ObjectProperty userInterfaceOrientation() { return userInterfaceOrientation; @@ -158,4 +171,24 @@ public class Settings { public BooleanProperty showTrayIcon() { return showTrayIcon; } + + public IntegerProperty windowXPositionProperty() { + return windowXPosition; + } + + public IntegerProperty windowYPositionProperty() { + return windowYPosition; + } + + public IntegerProperty windowWidthProperty() { + return windowWidth; + } + + public IntegerProperty windowHeightProperty() { + return windowHeight; + } + + public ObjectProperty displayConfigurationProperty() { + return displayConfiguration; + } } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 5bcb5f3d7..6d8d880e6 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -52,6 +52,12 @@ public class SettingsJsonAdapter extends TypeAdapter { out.name("licenseKey").value(value.licenseKey().get()); out.name("showMinimizeButton").value(value.showMinimizeButton().get()); out.name("showTrayIcon").value(value.showTrayIcon().get()); + out.name("windowXPosition").value((value.windowXPositionProperty().get())); + out.name("windowYPosition").value((value.windowYPositionProperty().get())); + out.name("windowWidth").value((value.windowWidthProperty().get())); + out.name("windowHeight").value((value.windowHeightProperty().get())); + out.name("displayConfiguration").value((value.displayConfigurationProperty().get())); + out.endObject(); } @@ -86,6 +92,12 @@ public class SettingsJsonAdapter extends TypeAdapter { case "licenseKey" -> settings.licenseKey().set(in.nextString()); case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean()); case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean()); + case "windowXPosition" -> settings.windowXPositionProperty().set(in.nextInt()); + case "windowYPosition" -> settings.windowYPositionProperty().set(in.nextInt()); + case "windowWidth" -> settings.windowWidthProperty().set(in.nextInt()); + case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt()); + case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString()); + default -> { LOG.warn("Unsupported vault setting found in JSON: " + name); in.skipValue(); diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 74ac7dc40..96bf3b252 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -17,9 +17,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProvider; -import org.cryptomator.cryptofs.VaultConfig; -import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig; -import org.cryptomator.cryptofs.VaultConfigLoadException; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; @@ -38,8 +35,6 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; @@ -62,6 +57,7 @@ public class Vault { private final AtomicReference cryptoFileSystem; private final VaultState state; private final ObjectProperty lastKnownException; + private final VaultConfigCache configCache; private final VaultStats stats; private final StringBinding displayName; private final StringBinding displayablePath; @@ -78,8 +74,9 @@ public class Vault { private volatile Volume volume; @Inject - Vault(VaultSettings vaultSettings, Provider volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats) { + Vault(VaultSettings vaultSettings, VaultConfigCache configCache, Provider volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats) { this.vaultSettings = vaultSettings; + this.configCache = configCache; this.volumeProvider = volumeProvider; this.defaultMountFlags = defaultMountFlags; this.cryptoFileSystem = cryptoFileSystem; @@ -107,10 +104,10 @@ public class Vault { Set flags = EnumSet.noneOf(FileSystemFlags.class); if (vaultSettings.usesReadOnlyMode().get()) { flags.add(FileSystemFlags.READONLY); - } else if(vaultSettings.maxCleartextFilenameLength().get() == -1) { + } else if (vaultSettings.maxCleartextFilenameLength().get() == -1) { LOG.debug("Determining cleartext filename length limitations..."); var checker = new FileSystemCapabilityChecker(); - int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold(); + int shorteningThreshold = configCache.get().allegedShorteningThreshold(); int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath()); if (ciphertextLimit < shorteningThreshold) { int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath()); @@ -328,19 +325,6 @@ public class Vault { return stats; } - /** - * Attempts to read the vault config file and parse it without verifying its integrity. - * - * @return an unverified vault config - * @throws VaultConfigLoadException if the read file cannot be properly parsed - * @throws IOException if reading the file fails - * - */ - public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException { - Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME); - String token = Files.readString(configPath, StandardCharsets.US_ASCII); - return VaultConfig.decode(token); - } public Observable[] observables() { return new Observable[]{state}; @@ -375,6 +359,10 @@ public class Vault { } } + public VaultConfigCache getVaultConfigCache() { + return configCache; + } + public void setCustomMountFlags(String mountFlags) { vaultSettings.mountFlags().set(mountFlags); } diff --git a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java index 588ff64cd..be844f510 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultComponent.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultComponent.java @@ -25,6 +25,9 @@ public interface VaultComponent { @BindsInstance Builder vaultSettings(VaultSettings vaultSettings); + @BindsInstance + Builder vaultConfigCache(VaultConfigCache configCache); + @BindsInstance Builder initialVaultState(VaultState.Value vaultState); diff --git a/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java new file mode 100644 index 000000000..80d70ffbf --- /dev/null +++ b/src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java @@ -0,0 +1,65 @@ +package org.cryptomator.common.vaults; + +import org.cryptomator.common.Constants; +import org.cryptomator.common.settings.VaultSettings; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.VaultConfigLoadException; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Wrapper for lazy loading and on-demand reloading of the vault configuration. + */ +public class VaultConfigCache { + + private final VaultSettings settings; + private final AtomicReference config; + + VaultConfigCache(VaultSettings settings) { + this.settings = settings; + this.config = new AtomicReference<>(null); + } + + void reloadConfig() throws IOException { + try { + config.set(readConfigFromStorage(this.settings.path().get())); + } catch (IOException e) { + config.set(null); + throw e; + } + } + + public VaultConfig.UnverifiedVaultConfig get() throws IOException { + if (config.get() == null) { + reloadConfig(); + } + return config.get(); + } + + public VaultConfig.UnverifiedVaultConfig getUnchecked() { + try { + return get(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + + /** + * Attempts to read the vault config file and parse it without verifying its integrity. + * + * @throws VaultConfigLoadException if the read file cannot be properly parsed + * @throws IOException if reading the file fails + */ + static VaultConfig.UnverifiedVaultConfig readConfigFromStorage(Path vaultPath) throws IOException { + Path configPath = vaultPath.resolve(Constants.VAULTCONFIG_FILENAME); + String token = Files.readString(configPath, StandardCharsets.US_ASCII); + return VaultConfig.decode(token); + } + +} diff --git a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index 05ea8ce07..6a8c31d4f 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -18,7 +18,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.io.IOException; import java.nio.file.Files; @@ -31,6 +30,7 @@ import java.util.ResourceBundle; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; import static org.cryptomator.common.vaults.VaultState.Value.ERROR; +import static org.cryptomator.common.vaults.VaultState.Value.LOCKED; @Singleton public class VaultListManager { @@ -96,6 +96,11 @@ public class VaultListManager { VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings); try { VaultState.Value vaultState = determineVaultState(vaultSettings.path().get()); + VaultConfigCache wrapper = new VaultConfigCache(vaultSettings); + compBuilder.vaultConfigCache(wrapper); //first set the wrapper in the builder, THEN try to load config + if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state + wrapper.reloadConfig(); + } compBuilder.initialVaultState(vaultState); } catch (IOException e) { LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e); @@ -112,6 +117,9 @@ public class VaultListManager { case LOCKED, NEEDS_MIGRATION, MISSING -> { try { var determinedState = determineVaultState(vault.getPath()); + if (determinedState == LOCKED) { + vault.getVaultConfigCache().reloadConfig(); + } state.set(determinedState); yield determinedState; } catch (IOException e) { @@ -132,7 +140,9 @@ public class VaultListManager { return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { case VAULT -> VaultState.Value.LOCKED; case UNRELATED -> VaultState.Value.MISSING; - case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING; + case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? // + VaultState.Value.NEEDS_MIGRATION // + : VaultState.Value.MISSING; }; } diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 39e25d503..35f2be069 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -29,16 +29,20 @@ import javafx.scene.control.ToggleGroup; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; +import java.util.UUID; @AddVaultWizardScoped public class CreateNewVaultLocationController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class); private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home")); + private static final String TEMP_FILE_FORMAT = "cryptomator-%s.tmp"; private final Stage window; private final Lazy chooseNameScene; @@ -92,7 +96,7 @@ public class CreateNewVaultLocationController implements FxController { statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist")); statusGraphic.set(badLocation); return false; - } else if (!Files.isWritable(p.getParent())) { + } else if (!isActuallyWritable(p.getParent())) { statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable")); statusGraphic.set(badLocation); return false; @@ -107,6 +111,21 @@ public class CreateNewVaultLocationController implements FxController { } } + private boolean isActuallyWritable(Path p) { + Path tmpFile = p.resolve(String.format(TEMP_FILE_FORMAT, UUID.randomUUID())); + try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) { + return true; + } catch (IOException e) { + return false; + } finally { + try { + Files.deleteIfExists(tmpFile); + } catch (IOException e) { + LOG.warn("Unable to delete temporary file {}. Needs to be deleted manually.", tmpFile); + } + } + } + @FXML public void initialize() { predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index 627793d6e..578b90969 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -182,7 +182,7 @@ public class CreateNewVaultPasswordController implements FxController { // 2. initialize vault: try { - MasterkeyLoader loader = ignored -> masterkey.clone(); + MasterkeyLoader loader = ignored -> masterkey.copy(); CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build(); CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID); diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java b/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java index 6cec655cb..313a31dc9 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java @@ -14,7 +14,7 @@ public class LocationPresets { private static final String USER_HOME = System.getProperty("user.home"); private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents", "~/iCloudDrive/iCloud~com~setolabs~Cryptomator"}; private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"}; - private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"}; + private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive/My Drive", "~/Google Drive"}; private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"}; private static final String[] MEGA_LOCATIONS = {"~/MEGA"}; private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"}; diff --git a/src/main/java/org/cryptomator/ui/common/ErrorController.java b/src/main/java/org/cryptomator/ui/common/ErrorController.java index 85b335b15..c75df26ce 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorController.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorController.java @@ -1,22 +1,47 @@ package org.cryptomator.ui.common; +import org.cryptomator.common.ErrorCode; import org.cryptomator.common.Nullable; import javax.inject.Inject; import javax.inject.Named; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.scene.Scene; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; public class ErrorController implements FxController { + private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s"; + private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s"; + private static final String SEARCH_ERRORCODE_DELIM = " OR "; + private static final String REPORT_BODY_TEMPLATE = """ + + + """; + + private final Application application; private final String stackTrace; + private final ErrorCode errorCode; private final Scene previousScene; private final Stage window; + private BooleanProperty copiedDetails = new SimpleBooleanProperty(); + @Inject - ErrorController(@Named("stackTrace") String stackTrace, @Nullable Scene previousScene, Stage window) { + ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window) { + this.application = application; this.stackTrace = stackTrace; + this.errorCode = errorCode; this.previousScene = previousScene; this.window = window; } @@ -33,6 +58,31 @@ public class ErrorController implements FxController { window.close(); } + @FXML + public void searchError() { + var searchTerm = URLEncoder.encode(getErrorCode().replace(ErrorCode.DELIM, SEARCH_ERRORCODE_DELIM), StandardCharsets.UTF_8); + application.getHostServices().showDocument(SEARCH_URL_FORMAT.formatted(searchTerm)); + } + + @FXML + public void reportError() { + var title = URLEncoder.encode(getErrorCode(), StandardCharsets.UTF_8); + var body = URLEncoder.encode(REPORT_BODY_TEMPLATE, StandardCharsets.UTF_8); + application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body)); + } + + @FXML + public void copyDetails() { + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(getDetailText()); + Clipboard.getSystemClipboard().setContent(clipboardContent); + + copiedDetails.set(true); + CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> { + copiedDetails.set(false); + }); + } + /* Getter/Setter */ public boolean isPreviousScenePresent() { @@ -42,4 +92,20 @@ public class ErrorController implements FxController { public String getStackTrace() { return stackTrace; } + + public String getErrorCode() { + return errorCode.toString(); + } + + public String getDetailText() { + return "```\nError Code " + getErrorCode() + "\n" + getStackTrace() + "\n```"; + } + + public BooleanProperty copiedDetailsProperty() { + return copiedDetails; + } + + public boolean getCopiedDetails() { + return copiedDetails.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/common/ErrorModule.java b/src/main/java/org/cryptomator/ui/common/ErrorModule.java index d2515e661..01b8790c1 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorModule.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorModule.java @@ -4,6 +4,7 @@ import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import org.cryptomator.common.ErrorCode; import javax.inject.Named; import javax.inject.Provider; @@ -31,6 +32,11 @@ abstract class ErrorModule { return baos.toString(StandardCharsets.UTF_8); } + @Provides + static ErrorCode provideErrorCode(Throwable cause) { + return ErrorCode.of(cause); + } + @Binds @IntoMap @FxControllerKey(ErrorController.class) diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index ea0c1ed38..b8d5bbff0 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -12,7 +12,6 @@ public enum FxmlFile { ERROR("/fxml/error.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // HEALTH_START("/fxml/health_start.fxml"), // - HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 15b1718e1..66eda7556 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -12,6 +12,7 @@ public enum FontAwesome5Icon { CARET_RIGHT("\uF0Da"), // CHECK("\uF00C"), // CLOCK("\uF017"), // + CLIPBOARD("\uF328"), // COG("\uF013"), // COGS("\uF085"), // COPY("\uF0C5"), // diff --git a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java index 3689b7e6e..0290f512d 100644 --- a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java @@ -10,6 +10,7 @@ package org.cryptomator.ui.controls; import com.google.common.base.Strings; +import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; @@ -27,7 +28,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; -import java.awt.Toolkit; import java.nio.CharBuffer; import java.text.Normalizer; import java.text.Normalizer.Form; @@ -123,8 +123,7 @@ public class SecurePasswordField extends TextField { } private void updateCapsLocked() { - //TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680) - capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)); + capsLocked.set(Platform.isKeyLocked(KeyCode.CAPS).orElse(false)); } private void updateContainingNonPrintableChars() { diff --git a/src/main/java/org/cryptomator/ui/health/CheckExecutor.java b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java index 5b14bd17c..a9ee9a17f 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckExecutor.java +++ b/src/main/java/org/cryptomator/ui/health/CheckExecutor.java @@ -67,7 +67,7 @@ public class CheckExecutor { @Override protected Void call() throws Exception { - try (var masterkeyClone = masterkey.clone(); // + try (var masterkeyClone = masterkey.copy(); // var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> { Platform.runLater(() -> c.getResults().add(Result.create(diagnosis))); diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java index f78e815c6..aa69e0828 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -16,26 +16,15 @@ import javafx.stage.Stage; @Subcomponent(modules = {HealthCheckModule.class}) public interface HealthCheckComponent { - LoadUnverifiedConfigResult loadConfig(); - @HealthCheckWindow Stage window(); @FxmlScene(FxmlFile.HEALTH_START) Lazy startScene(); - @FxmlScene(FxmlFile.HEALTH_START_FAIL) - Lazy failScene(); - default Stage showHealthCheckWindow() { Stage stage = window(); - // TODO reevaluate config loading, as soon as we have the new generic error screen - var unverifiedConf = loadConfig(); - if (unverifiedConf.config() != null) { - stage.setScene(startScene().get()); - } else { - stage.setScene(failScene().get()); - } + stage.setScene(startScene().get()); stage.show(); return stage; } @@ -52,5 +41,4 @@ public interface HealthCheckComponent { HealthCheckComponent build(); } - record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {} } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java index ad5ac6156..c36f486e0 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -27,7 +27,6 @@ import javafx.scene.Scene; import javafx.stage.Modality; import javafx.stage.Stage; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -37,18 +36,6 @@ import java.util.concurrent.atomic.AtomicReference; @Module(subcomponents = {KeyLoadingComponent.class}) abstract class HealthCheckModule { - // TODO reevaluate config loading, as soon as we have the new generic error screen - @Provides - @HealthCheckScoped - static HealthCheckComponent.LoadUnverifiedConfigResult provideLoadConfigResult(@HealthCheckWindow Vault vault) { - try { - return new HealthCheckComponent.LoadUnverifiedConfigResult(vault.getUnverifiedVaultConfig(), null); - } catch (IOException e) { - return new HealthCheckComponent.LoadUnverifiedConfigResult(null, e); - } - } - - @Provides @HealthCheckScoped static AtomicReference provideMasterkeyRef() { @@ -129,13 +116,6 @@ abstract class HealthCheckModule { return fxmlLoaders.createScene(FxmlFile.HEALTH_START); } - @Provides - @FxmlScene(FxmlFile.HEALTH_START_FAIL) - @HealthCheckScoped - static Scene provideHealthStartFailScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.HEALTH_START_FAIL); - } - @Provides @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) @HealthCheckScoped @@ -148,11 +128,6 @@ abstract class HealthCheckModule { @FxControllerKey(StartController.class) abstract FxController bindStartController(StartController controller); - @Binds - @IntoMap - @FxControllerKey(StartFailController.class) - abstract FxController bindStartFailController(StartFailController controller); - @Binds @IntoMap @FxControllerKey(CheckListController.class) diff --git a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java index 841a8f5c4..3dc91e33b 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java +++ b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java @@ -50,7 +50,7 @@ class ResultFixApplier { public void fix(DiagnosticResult diagnosis) { Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result"); - try (var masterkeyClone = masterkey.clone(); // + try (var masterkeyClone = masterkey.copy(); // var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); } catch (Exception e) { diff --git a/src/main/java/org/cryptomator/ui/health/StartController.java b/src/main/java/org/cryptomator/ui/health/StartController.java index ebba001b5..44c3f3a8f 100644 --- a/src/main/java/org/cryptomator/ui/health/StartController.java +++ b/src/main/java/org/cryptomator/ui/health/StartController.java @@ -1,7 +1,8 @@ package org.cryptomator.ui.health; -import com.google.common.base.Preconditions; import dagger.Lazy; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultConfigCache; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.VaultConfigLoadException; import org.cryptomator.cryptofs.VaultKeyInvalidException; @@ -18,8 +19,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; import javafx.scene.Scene; import javafx.stage.Stage; @@ -35,7 +34,7 @@ public class StartController implements FxController { private final Stage window; private final Stage unlockWindow; - private final ObjectProperty unverifiedVaultConfig; + private final VaultConfigCache vaultConfig; private final KeyLoadingStrategy keyLoadingStrategy; private final ExecutorService executor; private final AtomicReference masterkeyRef; @@ -44,11 +43,10 @@ public class StartController implements FxController { private final Lazy errorComponent; @Inject - public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent, @Named("unlockWindow") Stage unlockWindow) { - Preconditions.checkNotNull(configLoadResult.config()); + public StartController(@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent, @Named("unlockWindow") Stage unlockWindow) { this.window = window; this.unlockWindow = unlockWindow; - this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config()); + this.vaultConfig = vault.getVaultConfigCache(); this.keyLoadingStrategy = keyLoadingStrategy; this.executor = executor; this.masterkeyRef = masterkeyRef; @@ -71,7 +69,6 @@ public class StartController implements FxController { private void loadKey() { assert !Platform.isFxApplicationThread(); - assert unverifiedVaultConfig.get() != null; try { keyLoadingStrategy.use(this::verifyVaultConfig); } catch (VaultConfigLoadException | UnlockCancelledException e) { @@ -80,11 +77,11 @@ public class StartController implements FxController { } private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException { - var unverifiedCfg = unverifiedVaultConfig.get(); + var unverifiedCfg = vaultConfig.getUnchecked(); try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) { var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion()); vaultConfigRef.set(verifiedCfg); - var old = masterkeyRef.getAndSet(masterkey.clone()); + var old = masterkeyRef.getAndSet(masterkey.copy()); if (old != null) { old.destroy(); } diff --git a/src/main/java/org/cryptomator/ui/health/StartFailController.java b/src/main/java/org/cryptomator/ui/health/StartFailController.java deleted file mode 100644 index 826766026..000000000 --- a/src/main/java/org/cryptomator/ui/health/StartFailController.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.cryptomator.ui.health; - -import com.google.common.base.Preconditions; -import org.cryptomator.cryptofs.VaultConfigLoadException; -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.controls.FontAwesome5Icon; - -import javax.inject.Inject; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.fxml.FXML; -import javafx.scene.control.TitledPane; -import javafx.stage.Stage; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; - -// TODO reevaluate config loading, as soon as we have the new generic error screen -@HealthCheckScoped -public class StartFailController implements FxController { - - private final Stage window; - private final ObjectProperty loadError; - private final ObjectProperty moreInfoIcon; - - /* FXML */ - public TitledPane moreInfoPane; - - @Inject - public StartFailController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult) { - Preconditions.checkNotNull(configLoadResult.error()); - this.window = window; - this.loadError = new SimpleObjectProperty<>(configLoadResult.error()); - this.moreInfoIcon = new SimpleObjectProperty<>(FontAwesome5Icon.CARET_RIGHT); - } - - public void initialize() { - moreInfoPane.expandedProperty().addListener(this::setMoreInfoIcon); - } - - private void setMoreInfoIcon(ObservableValue observable, boolean wasExpanded, boolean willExpand) { - moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT); - } - - @FXML - public void close() { - window.close(); - } - - /* Getter & Setter */ - - public ObjectProperty moreInfoIconProperty() { - return moreInfoIcon; - } - - public FontAwesome5Icon getMoreInfoIcon() { - return moreInfoIcon.getValue(); - } - - public String getStackTrace() { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - loadError.get().printStackTrace(new PrintStream(baos)); - return baos.toString(StandardCharsets.UTF_8); - } - - public String getLocalizedErrorMessage() { - return loadError.get().getLocalizedMessage(); - } - - public boolean isParseException() { - return loadError.get() instanceof VaultConfigLoadException; - } - - public boolean isIoException() { - return !isParseException(); - } - -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java index f7eb8922f..bff757b1a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -3,7 +3,6 @@ package org.cryptomator.ui.keyloading; import dagger.Module; import dagger.Provides; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlLoaderFactory; @@ -11,9 +10,7 @@ import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule; import javax.inject.Provider; import java.io.IOException; -import java.net.URI; import java.util.Map; -import java.util.Optional; import java.util.ResourceBundle; @Module(includes = {MasterkeyFileLoadingModule.class}) @@ -31,7 +28,7 @@ abstract class KeyLoadingModule { @KeyLoadingScoped static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> strategies) { try { - String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme(); + String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme(); var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme)); return strategies.getOrDefault(scheme, () -> fallback).get(); } catch (IOException e) { diff --git a/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java b/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java index f4e5bb790..52ba838c0 100644 --- a/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java +++ b/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java @@ -11,12 +11,11 @@ import javax.inject.Named; import javax.inject.Singleton; import javafx.application.Platform; import java.io.IOException; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; -import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; +import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; @Singleton class AppLaunchEventHandler { @@ -69,7 +68,7 @@ class AppLaunchEventHandler { assert Platform.isFxApplicationThread(); try { final Vault v; - if (potentialVaultPath.getFileName().toString().equals(MASTERKEY_FILENAME)) { + if (potentialVaultPath.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { v = vaultListManager.add(potentialVaultPath.getParent()); } else { v = vaultListManager.add(potentialVaultPath); diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 392671246..c81aff125 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -23,12 +23,11 @@ import javafx.scene.layout.StackPane; import javafx.stage.Stage; import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Set; import java.util.stream.Collectors; +import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_EXT; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME; @@ -55,7 +54,7 @@ public class MainWindowController implements FxController { @FXML public void initialize() { - LOG.debug("init MainWindowController"); + LOG.trace("init MainWindowController"); root.setOnDragEntered(this::handleDragEvent); root.setOnDragOver(this::handleDragEvent); root.setOnDragDropped(this::handleDragEvent); @@ -96,6 +95,9 @@ public class MainWindowController implements FxController { private boolean containsVault(Path path) { try { + if (path.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { + path = path.getParent(); + } return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED; } catch (IOException e) { return false; @@ -104,7 +106,7 @@ public class MainWindowController implements FxController { private void addVault(Path pathToVault) { try { - if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) { + if (pathToVault.getFileName().toString().endsWith(CRYPTOMATOR_FILENAME_EXT)) { vaultListManager.add(pathToVault.getParent()); } else { vaultListManager.add(pathToVault); diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index 90311bd5b..545a45c04 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -6,17 +6,17 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.health.HealthCheckComponent; import org.cryptomator.ui.migration.MigrationComponent; import org.cryptomator.ui.removevault.RemoveVaultComponent; -import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import org.cryptomator.ui.stats.VaultStatisticsComponent; +import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import javax.inject.Provider; @@ -49,11 +49,8 @@ abstract class MainWindowModule { @MainWindowScoped static Stage provideStage(StageFactory factory) { Stage stage = factory.create(StageStyle.UNDECORATED); - // TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution... stage.setMinWidth(650); stage.setMinHeight(440); - stage.setMaxWidth(1000); - stage.setMaxHeight(700); stage.setTitle("Cryptomator"); return stage; } diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java index ef050e799..c8107415e 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -16,6 +16,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.fxml.FXML; +import javafx.scene.input.MouseButton; import javafx.scene.layout.HBox; import javafx.stage.Stage; @@ -53,22 +54,43 @@ public class MainWindowTitleController implements FxController { @FXML public void initialize() { - LOG.debug("init MainWindowTitleController"); + LOG.trace("init MainWindowTitleController"); updateChecker.automaticallyCheckForUpdatesIfEnabled(); titleBar.setOnMousePressed(event -> { xOffset = event.getSceneX(); yOffset = event.getSceneY(); + + }); + titleBar.setOnMouseClicked(event -> { + if (event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2) { + window.setFullScreen(!window.isFullScreen()); + } }); titleBar.setOnMouseDragged(event -> { + if (window.isFullScreen()) return; window.setX(event.getScreenX() - xOffset); window.setY(event.getScreenY() - yOffset); }); + titleBar.setOnDragDetected(mouseDragEvent -> { + titleBar.startFullDrag(); + }); + titleBar.setOnMouseDragReleased(mouseDragEvent -> { + saveWindowSettings(); + }); + window.setOnCloseRequest(event -> { close(); event.consume(); }); } + private void saveWindowSettings() { + settings.windowYPositionProperty().setValue(window.getY()); + settings.windowXPositionProperty().setValue(window.getX()); + settings.windowWidthProperty().setValue(window.getWidth()); + settings.windowHeightProperty().setValue(window.getHeight()); + } + @FXML public void close() { if (trayMenuInitialized) { diff --git a/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java index 908944d68..448d54d91 100644 --- a/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java +++ b/src/main/java/org/cryptomator/ui/mainwindow/ResizeController.java @@ -1,41 +1,100 @@ package org.cryptomator.ui.mainwindow; +import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.common.FxController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.geometry.Rectangle2D; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; +import javafx.stage.Screen; import javafx.stage.Stage; @MainWindow public class ResizeController implements FxController { + private static final Logger LOG = LoggerFactory.getLogger(ResizeController.class); + private final Stage window; public Region tlResizer; public Region trResizer; public Region blResizer; public Region brResizer; + public Region tResizer; + public Region rResizer; + public Region bResizer; + public Region lResizer; + public Region lDefaultRegion; + public Region tDefaultRegion; + public Region rDefaultRegion; + public Region bDefaultRegion; private double origX, origY, origW, origH; + private final Settings settings; + + private final BooleanBinding showResizingArrows; + @Inject - ResizeController(@MainWindow Stage window) { + ResizeController(@MainWindow Stage window, Settings settings) { this.window = window; - // TODO inject settings and save current position and size + this.settings = settings; + this.showResizingArrows = Bindings.createBooleanBinding(this::isShowResizingArrows, window.fullScreenProperty()); } @FXML public void initialize() { - tlResizer.setOnMousePressed(this::startResize); - trResizer.setOnMousePressed(this::startResize); - blResizer.setOnMousePressed(this::startResize); - brResizer.setOnMousePressed(this::startResize); - tlResizer.setOnMouseDragged(this::resizeTopLeft); - trResizer.setOnMouseDragged(this::resizeTopRight); - blResizer.setOnMouseDragged(this::resizeBottomLeft); - brResizer.setOnMouseDragged(this::resizeBottomRight); + LOG.trace("init ResizeController"); + + if (neverTouched()) { + settings.displayConfigurationProperty().setValue(getMonitorSizes()); + return; + } else { + if (didDisplayConfigurationChange()) { + //If the position is illegal, then the window appears on the main screen in the middle of the window. + Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds(); + window.setX((primaryScreenBounds.getWidth() - window.getMinWidth()) / 2); + window.setY((primaryScreenBounds.getHeight() - window.getMinHeight()) / 2); + window.setWidth(window.getMinWidth()); + window.setHeight(window.getMinHeight()); + } else { + window.setHeight(settings.windowHeightProperty().get() > window.getMinHeight() ? settings.windowHeightProperty().get() : window.getMinHeight()); + window.setWidth(settings.windowWidthProperty().get() > window.getMinWidth() ? settings.windowWidthProperty().get() : window.getMinWidth()); + window.setX(settings.windowXPositionProperty().get()); + window.setY(settings.windowYPositionProperty().get()); + } + } + savePositionalSettings(); + } + + private boolean neverTouched() { + return (settings.windowHeightProperty().get() == 0) && (settings.windowWidthProperty().get() == 0) && (settings.windowXPositionProperty().get() == 0) && (settings.windowYPositionProperty().get() == 0); + } + + private boolean didDisplayConfigurationChange() { + String currentDisplayConfiguration = getMonitorSizes(); + String settingsDisplayConfiguration = settings.displayConfigurationProperty().get(); + boolean configurationHasChanged = !settingsDisplayConfiguration.equals(currentDisplayConfiguration); + if (configurationHasChanged) settings.displayConfigurationProperty().setValue(currentDisplayConfiguration); + return configurationHasChanged; + } + + private String getMonitorSizes() { + ObservableList screens = Screen.getScreens(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < screens.size(); i++) { + Rectangle2D screenBounds = screens.get(i).getBounds(); + if (!sb.isEmpty()) sb.append(" "); + sb.append("displayId: " + i + ", " + screenBounds.getWidth() + "x" + screenBounds.getHeight() + ";"); + } + return sb.toString(); } private void startResize(MouseEvent evt) { @@ -45,27 +104,33 @@ public class ResizeController implements FxController { origH = window.getHeight(); } + @FXML private void resizeTopLeft(MouseEvent evt) { resizeTop(evt); resizeLeft(evt); } + @FXML private void resizeTopRight(MouseEvent evt) { resizeTop(evt); resizeRight(evt); } + @FXML private void resizeBottomLeft(MouseEvent evt) { resizeBottom(evt); resizeLeft(evt); } + @FXML private void resizeBottomRight(MouseEvent evt) { resizeBottom(evt); resizeRight(evt); } + @FXML private void resizeTop(MouseEvent evt) { + startResize(evt); double newY = evt.getScreenY(); double dy = newY - origY; double newH = origH - dy; @@ -75,7 +140,9 @@ public class ResizeController implements FxController { } } + @FXML private void resizeLeft(MouseEvent evt) { + startResize(evt); double newX = evt.getScreenX(); double dx = newX - origX; double newW = origW - dx; @@ -85,6 +152,7 @@ public class ResizeController implements FxController { } } + @FXML private void resizeBottom(MouseEvent evt) { double newH = evt.getSceneY(); if (newH < window.getMaxHeight() && newH > window.getMinHeight()) { @@ -92,6 +160,7 @@ public class ResizeController implements FxController { } } + @FXML private void resizeRight(MouseEvent evt) { double newW = evt.getSceneX(); if (newW < window.getMaxWidth() && newW > window.getMinWidth()) { @@ -99,4 +168,21 @@ public class ResizeController implements FxController { } } -} + @FXML + public void savePositionalSettings() { + settings.windowHeightProperty().setValue(window.getHeight()); + settings.windowWidthProperty().setValue(window.getWidth()); + settings.windowYPositionProperty().setValue(window.getY()); + settings.windowXPositionProperty().setValue(window.getX()); + } + + public BooleanBinding showResizingArrowsProperty() { + return showResizingArrows; + } + + public boolean isShowResizingArrows() { + //If in fullscreen resizing is not be possible; + return !window.isFullScreen(); + } + +} \ No newline at end of file diff --git a/src/main/resources/fxml/error.fxml b/src/main/resources/fxml/error.fxml index adcebdb67..4dbddc4c3 100644 --- a/src/main/resources/fxml/error.fxml +++ b/src/main/resources/fxml/error.fxml @@ -1,12 +1,15 @@ + + + @@ -15,7 +18,7 @@ fx:controller="org.cryptomator.ui.common.ErrorController" prefWidth="450" prefHeight="450" - spacing="12" + spacing="18" alignment="TOP_CENTER"> @@ -27,12 +30,38 @@ - -