From d7dda7d24902a9df864a8753a2856222db61b408 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 18 Feb 2019 16:53:41 +0100 Subject: [PATCH] Preparations for allowing to specify multiple paths to which Cryptomator writes settings, logs, etc. see #710 --- .../org/cryptomator/common/Environment.java | 73 ++++++++++++ .../common/settings/SettingsProvider.java | 75 +++++------- .../cryptomator/common/EnvironmentTest.java | 110 ++++++++++++++++++ 3 files changed, 213 insertions(+), 45 deletions(-) create mode 100644 main/commons/src/main/java/org/cryptomator/common/Environment.java create mode 100644 main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java diff --git a/main/commons/src/main/java/org/cryptomator/common/Environment.java b/main/commons/src/main/java/org/cryptomator/common/Environment.java new file mode 100644 index 000000000..e094f4257 --- /dev/null +++ b/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 getSettingsPath() { + return getPaths("cryptomator.settingsPath"); + } + + public Stream getIpcPortPath() { + return getPaths("cryptomator.ipcPortPath"); + } + + public Stream getKeychainPath() { + return getPaths("cryptomator.keychainPath"); + } + + // visible for testing + Stream getPaths(String propertyName) { + Stream 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 getRawList(String propertyName, char separator) { + String value = System.getProperty(propertyName); + if (value == null) { + return Stream.empty(); + } else { + Iterable iter = Splitter.on(separator).split(value); + Spliterator spliter = Spliterators.spliteratorUnknownSize(iter.iterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE); + return StreamSupport.stream(spliter, false); + } + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 48b85ef96..f10014db0 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/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 { 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> scheduledSaveCmd = new AtomicReference<>(); private final AtomicReference 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 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 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); diff --git a/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java b/main/commons/src/test/java/org/cryptomator/common/EnvironmentTest.java new file mode 100644 index 000000000..8fe899630 --- /dev/null +++ b/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 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 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 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 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 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 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 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"))); + } + + } + +}