Jelajahi Sumber

Feature: Retry if Read-only (#3695)

Closes #3261, closes #2085.

Co-authored-by: Armin Schrenk <armin.schrenk@skymatic.de>
mindmonk 4 bulan lalu
induk
melakukan
06988b06c7

+ 10 - 1
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -44,6 +44,7 @@ import javafx.beans.property.SimpleBooleanProperty;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.ReadOnlyFileSystemException;
 import java.util.EnumSet;
 import java.util.Objects;
 import java.util.Set;
@@ -111,7 +112,15 @@ public class Vault {
 
 	private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
 		Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
-		if (vaultSettings.usesReadOnlyMode.get()) {
+		var createReadOnly = vaultSettings.usesReadOnlyMode.get();
+		try {
+			FileSystemCapabilityChecker.assertWriteAccess(getPath());
+		} catch (FileSystemCapabilityChecker.MissingCapabilityException e) {
+			if (!createReadOnly) {
+				throw new ReadOnlyFileSystemException();
+			}
+		}
+		if (createReadOnly) {
 			flags.add(FileSystemFlags.READONLY);
 		} else if (vaultSettings.maxCleartextFilenameLength.get() == -1) {
 			LOG.debug("Determining cleartext filename length limitations...");

+ 10 - 8
src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java

@@ -40,7 +40,6 @@ 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.Optional;
 import java.util.ResourceBundle;
 import java.util.concurrent.ExecutorService;
@@ -50,7 +49,7 @@ 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 = ".locationTest.cryptomator.tmp";
+	private static final String TEMP_FILE_PREFIX = ".locationTest.cryptomator";
 
 	private final Stage window;
 	private final Lazy<Scene> chooseNameScene;
@@ -126,16 +125,19 @@ public class CreateNewVaultLocationController implements FxController {
 
 
 	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)) {
+		Path tmpDir = null;
+		try {
+			tmpDir = Files.createTempDirectory(p, TEMP_FILE_PREFIX );
 			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);
+			if (tmpDir != null) {
+				try {
+					Files.deleteIfExists(tmpDir);
+				} catch (IOException e) {
+					LOG.warn("Unable to delete temporary directory {}. Needs to be deleted manually.", tmpDir);
+				}
 			}
 		}
 	}

+ 12 - 0
src/main/java/org/cryptomator/ui/dialogs/Dialogs.java

@@ -72,4 +72,16 @@ public class Dialogs {
 				.setCancelAction(cancelAction);
 	}
 
+	public SimpleDialog.Builder prepareRetryIfReadonlyDialog(Stage window, Consumer<Stage> okAction) {
+		return createDialogBuilder() //
+				.setOwner(window) //
+				.setTitleKey("retryIfReadonly.title") //
+				.setMessageKey("retryIfReadonly.message") //
+				.setDescriptionKey("retryIfReadonly.description") //
+				.setIcon(FontAwesome5Icon.EXCLAMATION) //
+				.setOkButtonKey("retryIfReadonly.retry") //
+				.setCancelButtonKey("generic.button.close") //
+				.setOkAction(okAction) //
+				.setCancelAction(Stage::close);
+	}
 }

+ 32 - 1
src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java

@@ -10,6 +10,7 @@ import org.cryptomator.integrations.mount.MountFailedException;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.dialogs.Dialogs;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.fxapp.PrimaryStage;
 import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
@@ -25,6 +26,8 @@ import javafx.scene.Scene;
 import javafx.stage.Screen;
 import javafx.stage.Stage;
 import java.io.IOException;
+import java.nio.file.ReadOnlyFileSystemException;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A multi-step task that consists of background activities as well as user interaction.
@@ -46,6 +49,7 @@ public class UnlockWorkflow extends Task<Void> {
 	private final FxApplicationWindows appWindows;
 	private final KeyLoadingStrategy keyLoadingStrategy;
 	private final ObjectProperty<IllegalMountPointException> illegalMountPointException;
+	private final Dialogs dialogs;
 
 	@Inject
 	UnlockWorkflow(@PrimaryStage Stage mainWindow, //
@@ -57,7 +61,8 @@ public class UnlockWorkflow extends Task<Void> {
 				   @FxmlScene(FxmlFile.UNLOCK_REQUIRES_RESTART) Lazy<Scene> restartRequiredScene, //
 				   FxApplicationWindows appWindows, //
 				   @UnlockWindow KeyLoadingStrategy keyLoadingStrategy, //
-				   @UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException) {
+				   @UnlockWindow ObjectProperty<IllegalMountPointException> illegalMountPointException, //
+				   Dialogs dialogs) {
 		this.mainWindow = mainWindow;
 		this.window = window;
 		this.vault = vault;
@@ -68,6 +73,7 @@ public class UnlockWorkflow extends Task<Void> {
 		this.appWindows = appWindows;
 		this.keyLoadingStrategy = keyLoadingStrategy;
 		this.illegalMountPointException = illegalMountPointException;
+		this.dialogs = dialogs;
 	}
 
 	@Override
@@ -144,11 +150,36 @@ public class UnlockWorkflow extends Task<Void> {
 		switch (throwable) {
 			case IllegalMountPointException e -> handleIllegalMountPointError(e);
 			case ConflictingMountServiceException _ -> handleConflictingMountServiceException();
+			case ReadOnlyFileSystemException _ -> handleReadOnlyFileSystem();
 			default -> handleGenericError(throwable);
 		}
 		vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
 	}
 
+	private void handleReadOnlyFileSystem() {
+		var readOnlyDialog = dialogs.prepareRetryIfReadonlyDialog(mainWindow, stage -> {
+			stage.close();
+			this.retry();
+		}).build();
+
+		Platform.runLater(readOnlyDialog::showAndWait);
+	}
+
+	private void retry() {
+		try {
+			vault.getVaultSettings().usesReadOnlyMode.set(true);
+			var isLocked = vault.stateProperty().awaitState(VaultState.Value.LOCKED, 5, TimeUnit.SECONDS);
+			if (!isLocked) {
+				LOG.error("Vault did not changed to LOCKED state within 5 seconds. Aborting unlock retry.");
+			} else {
+				appWindows.startUnlockWorkflow(vault, mainWindow);
+			}
+		} catch (InterruptedException e) {
+			LOG.error("Waiting for LOCKED vault state was interrupted. Aborting unlock retry.", e);
+			Thread.currentThread().interrupt();
+		}
+	}
+
 	@Override
 	protected void cancelled() {
 		LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());

+ 6 - 0
src/main/resources/i18n/strings.properties

@@ -553,6 +553,12 @@ dokanySupportEnd.message=Support end for Dokany
 dokanySupportEnd.description=The volume type Dokany is no longer supported by Cryptomator. Your settings are adjusted to use the default volume type now. You can view the default type in the preferences.
 dokanySupportEnd.preferencesBtn=Open Preferences
 
+#Retry If Readonly
+retryIfReadonly.title=Restricted Vault Access
+retryIfReadonly.message=No write access to vault directory
+retryIfReadonly.description=Cryptomator cannot write to the vault directory. You can change the vault to be read-only and try again. This option can be disabled in the vault options.
+retryIfReadonly.retry=Change and Retry
+
 # Share Vault
 shareVault.title=Share Vault
 shareVault.message=Would you like to share your vault with others?