소스 검색

Preparations for allowing to specify multiple paths to which Cryptomator writes settings, logs, etc. see #710

Sebastian Stenzel 6 년 전
부모
커밋
d7dda7d249

+ 73 - 0
main/commons/src/main/java/org/cryptomator/common/Environment.java

@@ -0,0 +1,73 @@
+package org.cryptomator.common;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+@Singleton
+public class Environment {
+
+	private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
+	private static final String USER_HOME = System.getProperty("user.home");
+	private static final Path RELATIVE_HOME_DIR = Paths.get("~");
+	private static final Path ABSOLUTE_HOME_DIR = Paths.get(USER_HOME);
+	private static final char PATH_LIST_SEP = ':';
+
+	@Inject
+	public Environment() {
+		LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
+		LOG.debug("cryptomator.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath"));
+		LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.ipcPortPath"));
+
+	}
+
+	public Stream<Path> getSettingsPath() {
+		return getPaths("cryptomator.settingsPath");
+	}
+
+	public Stream<Path> getIpcPortPath() {
+		return getPaths("cryptomator.ipcPortPath");
+	}
+
+	public Stream<Path> getKeychainPath() {
+		return getPaths("cryptomator.keychainPath");
+	}
+
+	// visible for testing
+	Stream<Path> getPaths(String propertyName) {
+		Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
+		return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
+	}
+
+	private Path replaceHomeDir(Path path) {
+		if (path.startsWith(RELATIVE_HOME_DIR)) {
+			return ABSOLUTE_HOME_DIR.resolve(RELATIVE_HOME_DIR.relativize(path));
+		} else {
+			return path;
+		}
+	}
+
+	private Stream<String> getRawList(String propertyName, char separator) {
+		String value = System.getProperty(propertyName);
+		if (value == null) {
+			return Stream.empty();
+		} else {
+			Iterable<String> iter = Splitter.on(separator).split(value);
+			Spliterator<String> spliter = Spliterators.spliteratorUnknownSize(iter.iterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE);
+			return StreamSupport.stream(spliter, false);
+		}
+	}
+
+}

+ 30 - 45
main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java

@@ -27,6 +27,7 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -34,6 +35,7 @@ import javax.inject.Singleton;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
 import org.cryptomator.common.LazyInitializer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,92 +47,75 @@ import com.google.gson.GsonBuilder;
 public class SettingsProvider implements Provider<Settings> {
 
 	private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
-	private static final Path DEFAULT_SETTINGS_PATH;
 	private static final long SAVE_DELAY_MS = 1000;
 
-	static {
-		final FileSystem fs = FileSystems.getDefault();
-		if (SystemUtils.IS_OS_WINDOWS) {
-			DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "AppData/Roaming/Cryptomator/settings.json");
-		} else if (SystemUtils.IS_OS_MAC_OSX) {
-			DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator/settings.json");
-		} else {
-			DEFAULT_SETTINGS_PATH = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator/settings.json");
-		}
-	}
-
 	private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
 	private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
 	private final AtomicReference<Settings> settings = new AtomicReference<>();
 	private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter();
+	private final Environment env;
 	private final Gson gson;
 
 	@Inject
-	public SettingsProvider() {
+	public SettingsProvider(Environment env) {
+		this.env = env;
 		this.gson = new GsonBuilder() //
 				.setPrettyPrinting().setLenient().disableHtmlEscaping() //
 				.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
 				.create();
 	}
 
-	private Path getSettingsPath() {
-		final String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
-		return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH);
-	}
-
-	private String replaceHomeDir(String path) {
-		if (path.startsWith("~/")) {
-			return SystemUtils.USER_HOME + path.substring(1);
-		} else {
-			return path;
-		}
-	}
-
 	@Override
 	public Settings get() {
 		return LazyInitializer.initializeLazily(settings, this::load);
 	}
 
 	private Settings load() {
-		Settings settings;
-		final Path settingsPath = getSettingsPath();
-		try (InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); //
-				Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
-			settings = gson.fromJson(reader, Settings.class);
+		Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings());
+		settings.setSaveCmd(this::scheduleSave);
+		return settings;
+	}
+
+	private Stream<Settings> tryLoad(Path path) {
+		LOG.debug("Attempting to load settings from {}", path);
+		try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ); //
+			 Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
+			Settings settings = gson.fromJson(reader, Settings.class);
 			if (settings == null) {
 				throw new IOException("Unexpected EOF");
 			}
-			LOG.info("Settings loaded from " + settingsPath);
+			LOG.info("Settings loaded from {}", path);
+			return Stream.of(settings);
 		} catch (IOException e) {
 			LOG.info("Failed to load settings, creating new one.");
-			settings = new Settings();
+			return Stream.empty();
 		}
-		settings.setSaveCmd(this::scheduleSave);
-		return settings;
 	}
 
 	private void scheduleSave(Settings settings) {
 		if (settings == null) {
 			return;
 		}
-		ScheduledFuture<?> saveCmd = saveScheduler.schedule(() -> {
-			this.save(settings);
-		}, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
-		ScheduledFuture<?> previousSaveCmd = scheduledSaveCmd.getAndSet(saveCmd);
-		if (previousSaveCmd != null) {
-			previousSaveCmd.cancel(false);
-		}
+		final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
+		settingsPath.ifPresent(path -> {
+			Runnable saveCommand = () -> this.save(settings, path);
+			ScheduledFuture<?> scheduledTask = saveScheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
+			ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
+			if (previouslyScheduledTask != null) {
+				previouslyScheduledTask.cancel(false);
+			}
+		});
 	}
 
-	private void save(Settings settings) {
+	private void save(Settings settings, Path settingsPath) {
 		assert settings != null : "method should only be invoked by #scheduleSave, which checks for null";
-		final Path settingsPath = getSettingsPath();
+		LOG.debug("Attempting to save settings to {}", settingsPath);
 		try {
 			Files.createDirectories(settingsPath.getParent());
 			try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
 					Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
 				gson.toJson(settings, writer);
-				LOG.info("Settings saved to " + settingsPath);
+				LOG.info("Settings saved to {}", settingsPath);
 			}
 		} catch (IOException e) {
 			LOG.error("Failed to save settings.", e);

+ 110 - 0
main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java

@@ -0,0 +1,110 @@
+package org.cryptomator.common;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@DisplayName("Environment Variables Test")
+class EnvironmentTest {
+
+	private Environment env;
+
+	@BeforeAll
+	static void init() {
+		System.setProperty("user.home", "/home/testuser");
+	}
+
+	@BeforeEach
+	void initEach() {
+		env = new Environment();
+	}
+
+	@Test
+	@DisplayName("cryptomator.settingsPath=~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json")
+	public void testSettingsPath() {
+		System.setProperty("cryptomator.settingsPath", "~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json");
+
+		List<Path> result = env.getSettingsPath().collect(Collectors.toList());
+		MatcherAssert.assertThat(result, Matchers.hasSize(2));
+		MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/settings.json"),
+				Paths.get("/home/testuser/.Cryptomator/settings.json")));
+	}
+
+	@Test
+	@DisplayName("cryptomator.ipcPortPath=~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin")
+	public void testIpcPortPath() {
+		System.setProperty("cryptomator.ipcPortPath", "~/.config/Cryptomator/ipcPort.bin:~/.Cryptomator/ipcPort.bin");
+
+		List<Path> result = env.getIpcPortPath().collect(Collectors.toList());
+		MatcherAssert.assertThat(result, Matchers.hasSize(2));
+		MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/.config/Cryptomator/ipcPort.bin"),
+				Paths.get("/home/testuser/.Cryptomator/ipcPort.bin")));
+	}
+
+	@Test
+	@DisplayName("cryptomator.keychainPath=~/AppData/Roaming/Cryptomator/keychain.json")
+	public void testKeychainPath() {
+		System.setProperty("cryptomator.keychainPath", "~/AppData/Roaming/Cryptomator/keychain.json");
+
+		List<Path> result = env.getKeychainPath().collect(Collectors.toList());
+		MatcherAssert.assertThat(result, Matchers.hasSize(1));
+		MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/AppData/Roaming/Cryptomator/keychain.json")));
+	}
+
+	@Nested
+	@DisplayName("Path Lists")
+	class SettingsPath {
+
+		@Test
+		@DisplayName("test.path.property=")
+		public void testEmptyList() {
+			System.setProperty("test.path.property", "");
+			List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
+
+			MatcherAssert.assertThat(result, Matchers.hasSize(0));
+		}
+
+		@Test
+		@DisplayName("test.path.property=/foo/bar/test")
+		public void testSingleAbsolutePath() {
+			System.setProperty("test.path.property", "/foo/bar/test");
+			List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
+
+			MatcherAssert.assertThat(result, Matchers.hasSize(1));
+			MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/foo/bar/test")));
+		}
+
+		@Test
+		@DisplayName("test.path.property=~/test")
+		public void testSingleHomeRelativePath() {
+			System.setProperty("test.path.property", "~/test");
+			List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
+
+			MatcherAssert.assertThat(result, Matchers.hasSize(1));
+			MatcherAssert.assertThat(result, Matchers.hasItem(Paths.get("/home/testuser/test")));
+		}
+
+		@Test
+		@DisplayName("test.path.property=~/test:~/test2:/foo/bar/test")
+		public void testMultiplePaths() {
+			System.setProperty("test.path.property", "~/test:~/test2:/foo/bar/test");
+			List<Path> result = env.getPaths("test.path.property").collect(Collectors.toList());
+
+			MatcherAssert.assertThat(result, Matchers.hasSize(3));
+			MatcherAssert.assertThat(result, Matchers.contains(Paths.get("/home/testuser/test"),
+					Paths.get("/home/testuser/test2"),
+					Paths.get("/foo/bar/test")));
+		}
+
+	}
+
+}