diff --git a/.travis.yml b/.travis.yml index ff30b781b..611045f47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' ht script: mvn -fmain/pom.xml clean test -after_success: mvn -fmain/pom.xml clean test jacoco:report coveralls:report +after_success: mvn -fmain/pom.xml -Ptest-coverage clean test jacoco:report-aggregate coveralls:report notifications: webhooks: diff --git a/main/ant-kit/src/main/resources/package/linux/Cryptomator.png b/main/ant-kit/src/main/resources/package/linux/Cryptomator.png index 99d9fb876..1e0832c42 100644 Binary files a/main/ant-kit/src/main/resources/package/linux/Cryptomator.png and b/main/ant-kit/src/main/resources/package/linux/Cryptomator.png differ diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 81ef3d6ef..5603f36b5 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -17,11 +17,28 @@ Shared utilities + com.google.guava guava - + + org.apache.commons + commons-lang3 + + + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + + junit junit @@ -43,4 +60,13 @@ test + + + + + org.jacoco + jacoco-maven-plugin + + + diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java new file mode 100644 index 000000000..d56d22f80 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -0,0 +1,21 @@ +package org.cryptomator.common; + +import java.util.Comparator; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class CommonsModule { + + @Provides + @Singleton + @Named("SemVer") + Comparator providesSemVerComparator() { + return new SemVerComparator(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java similarity index 97% rename from main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java rename to main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java index b9031b471..930e2e93d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java +++ b/main/commons/src/main/java/org/cryptomator/common/SemVerComparator.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.ui.util; +package org.cryptomator.common; import java.util.Comparator; diff --git a/main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java b/main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java similarity index 95% rename from main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java rename to main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java index a505d0af4..859eb9471 100644 --- a/main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/SemVerComparatorTest.java @@ -6,10 +6,11 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.ui.util; +package org.cryptomator.common; import java.util.Comparator; +import org.cryptomator.common.SemVerComparator; import org.junit.Assert; import org.junit.Test; diff --git a/main/filesystem-charsets/pom.xml b/main/filesystem-charsets/pom.xml new file mode 100644 index 000000000..b5eba0555 --- /dev/null +++ b/main/filesystem-charsets/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + org.cryptomator + main + 1.1.0-SNAPSHOT + + filesystem-charsets + Cryptomator filesystem: Filename charset compatibility layer + + + + org.cryptomator + filesystem-api + + + + + org.cryptomator + commons-test + + + org.cryptomator + filesystem-inmemory + + + + + + + org.jacoco + jacoco-maven-plugin + + + + \ No newline at end of file diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java new file mode 100644 index 000000000..af73d59c1 --- /dev/null +++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.io.UncheckedIOException; +import java.text.Normalizer; +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.delegating.DelegatingFile; + +class NormalizedNameFile extends DelegatingFile { + + private final Form displayForm; + + public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) { + super(parent, delegate); + this.displayForm = displayForm; + } + + @Override + public String name() throws UncheckedIOException { + return Normalizer.normalize(super.name(), displayForm); + } + +} diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java new file mode 100644 index 000000000..a69c572d6 --- /dev/null +++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.Folder; +import org.cryptomator.filesystem.delegating.DelegatingFileSystem; + +public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem { + + public NormalizedNameFileSystem(Folder delegate, Form displayForm) { + super(null, delegate, displayForm); + } + + @Override + public Folder getDelegate() { + return delegate; + } + +} diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java new file mode 100644 index 000000000..e2762059b --- /dev/null +++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.io.UncheckedIOException; +import java.text.Normalizer; +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.cryptomator.filesystem.delegating.DelegatingFolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class NormalizedNameFolder extends DelegatingFolder { + + private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class); + private final Form displayForm; + + public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) { + super(parent, delegate); + this.displayForm = displayForm; + } + + @Override + public String name() throws UncheckedIOException { + return Normalizer.normalize(super.name(), displayForm); + } + + @Override + public NormalizedNameFile file(String name) throws UncheckedIOException { + String nfcName = Normalizer.normalize(name, Form.NFC); + String nfdName = Normalizer.normalize(name, Form.NFD); + NormalizedNameFile nfcFile = super.file(nfcName); + NormalizedNameFile nfdFile = super.file(nfdName); + if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) { + LOG.warn("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC)."); + } else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) { + LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC)."); + nfdFile.moveTo(nfcFile); + } + return nfcFile; + } + + @Override + protected NormalizedNameFile newFile(File delegate) { + return new NormalizedNameFile(this, delegate, displayForm); + } + + @Override + public NormalizedNameFolder folder(String name) throws UncheckedIOException { + String nfcName = Normalizer.normalize(name, Form.NFC); + String nfdName = Normalizer.normalize(name, Form.NFD); + NormalizedNameFolder nfcFolder = super.folder(nfcName); + NormalizedNameFolder nfdFolder = super.folder(nfdName); + if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) { + LOG.warn("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC)."); + } else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) { + LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC)."); + nfdFolder.moveTo(nfcFolder); + } + return nfcFolder; + } + + @Override + protected NormalizedNameFolder newFolder(Folder delegate) { + return new NormalizedNameFolder(this, delegate, displayForm); + } + +} diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java new file mode 100644 index 000000000..3ee4f6a17 --- /dev/null +++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +/** + * Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C. + * Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form. + * It is recommended to use NFD for OS X and NFC for other operating systems. + * When looking for a file or folder with a name given in either form, both possibilities are considered + * and files/folders stored in NFD are automatically migrated to NFC. + */ +package org.cryptomator.filesystem.charsets; \ No newline at end of file diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java new file mode 100644 index 000000000..e662838a8 --- /dev/null +++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.nio.ByteBuffer; +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.WritableFile; +import org.cryptomator.filesystem.inmem.InMemoryFileSystem; +import org.junit.Assert; +import org.junit.Test; + +public class NormalizedNameFileSystemTest { + + @Test + public void testFileMigration() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) { + writable.write(ByteBuffer.allocate(0)); + } + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC); + Assert.assertTrue(normalizationFs.file("\u00F4").exists()); + Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists()); + Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists()); + Assert.assertTrue(inMemoryFs.file("\u00F4").exists()); + } + + @Test + public void testNoFileMigration() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) { + writable.write(ByteBuffer.allocate(0)); + } + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC); + Assert.assertTrue(normalizationFs.file("\u00F4").exists()); + Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists()); + Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists()); + Assert.assertTrue(inMemoryFs.file("\u00F4").exists()); + } + + @Test + public void testFolderMigration() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + inMemoryFs.folder("\u006F\u0302").create(); + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC); + Assert.assertTrue(normalizationFs.folder("\u00F4").exists()); + Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists()); + Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists()); + Assert.assertTrue(inMemoryFs.folder("\u00F4").exists()); + } + + @Test + public void testNoFolderMigration() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + inMemoryFs.folder("\u00F4").create(); + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC); + Assert.assertTrue(normalizationFs.folder("\u00F4").exists()); + Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists()); + Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists()); + Assert.assertTrue(inMemoryFs.folder("\u00F4").exists()); + } + + @Test + public void testNfcDisplayNames() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + inMemoryFs.folder("a\u00F4").create(); + inMemoryFs.folder("b\u006F\u0302").create(); + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC); + Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name()); + Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name()); + } + + @Test + public void testNfdDisplayNames() { + FileSystem inMemoryFs = new InMemoryFileSystem(); + inMemoryFs.folder("a\u00F4").create(); + inMemoryFs.folder("b\u006F\u0302").create(); + FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD); + Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name()); + Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name()); + } + +} diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java new file mode 100644 index 000000000..3a3f1fe64 --- /dev/null +++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.File; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class NormalizedNameFileTest { + + private File delegateNfc; + private File delegateNfd; + + @Before + public void setup() { + delegateNfc = Mockito.mock(File.class); + delegateNfd = Mockito.mock(File.class); + Mockito.when(delegateNfc.name()).thenReturn("\u00C5"); + Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A"); + } + + @Test + public void testDisplayNameInNfc() { + File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC); + File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC); + Assert.assertEquals("\u00C5", file1.name()); + Assert.assertEquals("\u00C5", file2.name()); + } + + @Test + public void testDisplayNameInNfd() { + File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD); + File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD); + Assert.assertEquals("\u0041\u030A", file1.name()); + Assert.assertEquals("\u0041\u030A", file2.name()); + } + +} diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java new file mode 100644 index 000000000..7bf017652 --- /dev/null +++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2016 Sebastian Stenzel and others. + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.filesystem.charsets; + +import java.text.Normalizer.Form; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class NormalizedNameFolderTest { + + private Folder delegate; + private File delegateSubFileNfc; + private File delegateSubFileNfd; + private Folder delegateSubFolderNfc; + private Folder delegateSubFolderNfd; + + @Before + public void setup() { + delegate = Mockito.mock(Folder.class); + delegateSubFileNfc = Mockito.mock(File.class); + delegateSubFileNfd = Mockito.mock(File.class); + Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc); + Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5"); + Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd); + Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A"); + delegateSubFolderNfc = Mockito.mock(Folder.class); + delegateSubFolderNfd = Mockito.mock(Folder.class); + Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc); + Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4"); + Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd); + Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302"); + } + + @Test + public void testDisplayNameInNfc() { + Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC); + Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC); + Assert.assertEquals("\u00F4", folder1.name()); + Assert.assertEquals("\u00F4", folder2.name()); + } + + @Test + public void testDisplayNameInNfd() { + Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD); + Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD); + Assert.assertEquals("\u006F\u0302", folder1.name()); + Assert.assertEquals("\u006F\u0302", folder2.name()); + } + + @Test + public void testNoFolderMigration1() { + Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true); + Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + Folder sub1 = folder.folder("\u00F4"); + Folder sub2 = folder.folder("\u006F\u0302"); + Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testNoFolderMigration2() { + Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true); + Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + Folder sub1 = folder.folder("\u00F4"); + Folder sub2 = folder.folder("\u006F\u0302"); + Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testNoFolderMigration3() { + Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false); + Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + Folder sub1 = folder.folder("\u00F4"); + Folder sub2 = folder.folder("\u006F\u0302"); + Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testFolderMigration() { + Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false); + Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + Folder sub1 = folder.folder("\u00F4"); + Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc); + Folder sub2 = folder.folder("\u006F\u0302"); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testNoFileMigration1() { + Mockito.when(delegateSubFileNfc.exists()).thenReturn(true); + Mockito.when(delegateSubFileNfd.exists()).thenReturn(false); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + File sub1 = folder.file("\u00C5"); + File sub2 = folder.file("\u0041\u030A"); + Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testNoFileMigration2() { + Mockito.when(delegateSubFileNfc.exists()).thenReturn(true); + Mockito.when(delegateSubFileNfd.exists()).thenReturn(true); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + File sub1 = folder.file("\u00C5"); + File sub2 = folder.file("\u0041\u030A"); + Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testNoFileMigration3() { + Mockito.when(delegateSubFileNfc.exists()).thenReturn(false); + Mockito.when(delegateSubFileNfd.exists()).thenReturn(false); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + File sub1 = folder.file("\u00C5"); + File sub2 = folder.file("\u0041\u030A"); + Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(sub1, sub2); + } + + @Test + public void testFileMigration() { + Mockito.when(delegateSubFileNfc.exists()).thenReturn(false); + Mockito.when(delegateSubFileNfd.exists()).thenReturn(true); + Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC); + File sub1 = folder.file("\u00C5"); + Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc); + File sub2 = folder.file("\u0041\u030A"); + Assert.assertSame(sub1, sub2); + } + +} diff --git a/main/filesystem-charsets/src/test/resources/log4j2.xml b/main/filesystem-charsets/src/test/resources/log4j2.xml new file mode 100644 index 000000000..9b4889392 --- /dev/null +++ b/main/filesystem-charsets/src/test/resources/log4j2.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/filesystem-invariants-tests/pom.xml b/main/filesystem-invariants-tests/pom.xml index aa13917ee..054464338 100644 --- a/main/filesystem-invariants-tests/pom.xml +++ b/main/filesystem-invariants-tests/pom.xml @@ -20,6 +20,10 @@ org.cryptomator filesystem-api + + org.cryptomator + filesystem-charsets + org.cryptomator filesystem-crypto diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java index 5f9a423f5..8f1eed8ca 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java @@ -4,11 +4,13 @@ import static org.cryptomator.common.test.TempFilesRemovedOnShutdown.createTempD import java.io.IOException; import java.io.UncheckedIOException; +import java.text.Normalizer.Form; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem; import org.cryptomator.filesystem.crypto.CryptoEngineTestModule; import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate; import org.cryptomator.filesystem.crypto.CryptoFileSystemTestComponent; @@ -35,8 +37,10 @@ class FileSystemFactories implements Iterable { add("ShorteningFileSystem > InMemoryFileSystem", this::createShorteningFileSystemInMemory); add("StatsFileSystem > NioFileSystem", this::createStatsFileSystemNio); add("StatsFileSystem > InMemoryFileSystem", this::createStatsFileSystemInMemory); - add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory); - add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio); + add("NormalizingFileSystem > NioFileSystem", this::createNormalizingFileSystemNio); + add("NormalizingFileSystem > InMemoryFileSystem", this::createNormalizingFileSystemInMemory); + add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory); + add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio); } private FileSystem createCryptoFileSystemInMemory() { @@ -63,6 +67,14 @@ class FileSystemFactories implements Iterable { return createStatsFileSystem(createInMemoryFileSystem()); } + private FileSystem createNormalizingFileSystemNio() { + return createNormalizingFileSystem(createInMemoryFileSystem()); + } + + private FileSystem createNormalizingFileSystemInMemory() { + return createNormalizingFileSystem(createInMemoryFileSystem()); + } + private FileSystem createCompoundFileSystemNio() { return createCompoundFileSystem(createNioFileSystem()); } @@ -84,13 +96,17 @@ class FileSystemFactories implements Iterable { } private FileSystem createCompoundFileSystem(FileSystem delegate) { - return createStatsFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate))); + return createStatsFileSystem(createNormalizingFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate)))); } private FileSystem createStatsFileSystem(FileSystem delegate) { return new StatsFileSystem(delegate); } + private FileSystem createNormalizingFileSystem(FileSystem delegate) { + return new NormalizedNameFileSystem(delegate, Form.NFC); + } + private FileSystem createCryptoFileSystem(FileSystem delegate) { CRYPTO_FS_COMP.cryptoFileSystemFactory().initializeNew(delegate, "aPassphrase"); return CRYPTO_FS_COMP.cryptoFileSystemFactory().unlockExisting(delegate, "aPassphrase", Mockito.mock(CryptoFileSystemDelegate.class)); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java index 9f6c5dbff..68f1a43e7 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/WebDavComponent.java @@ -10,10 +10,12 @@ package org.cryptomator.frontend.webdav; import javax.inject.Singleton; +import org.cryptomator.common.CommonsModule; + import dagger.Component; @Singleton -@Component +@Component(modules = {CommonsModule.class}) public interface WebDavComponent { WebDavServer server(); diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java similarity index 91% rename from main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java rename to main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java index 0acf2ddee..f381e2e6f 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXAppleScriptWebDavMounter.java @@ -12,11 +12,13 @@ package org.cryptomator.frontend.webdav.mount; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Comparator; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.io.IOUtils; @@ -26,15 +28,18 @@ import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.Frontend.MountParam; @Singleton -final class MacOsXWebDavMounter implements WebDavMounterStrategy { +final class MacOsXAppleScriptWebDavMounter implements WebDavMounterStrategy { + + private final Comparator semVerComparator; @Inject - MacOsXWebDavMounter() { + MacOsXAppleScriptWebDavMounter(@Named("SemVer") Comparator semVerComparator) { + this.semVerComparator = semVerComparator; } @Override public boolean shouldWork() { - return SystemUtils.IS_OS_MAC_OSX; + return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") >= 0; } @Override diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java new file mode 100644 index 000000000..89e8a86a2 --- /dev/null +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MacOsXShellScriptWebDavMounter.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2014, 2016 Sebastian Stenzel, Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation, strategy fine tuning + * Markus Kreusch - Refactored WebDavMounter to use strategy pattern + ******************************************************************************/ +package org.cryptomator.frontend.webdav.mount; + +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.frontend.CommandFailedException; +import org.cryptomator.frontend.Frontend.MountParam; +import org.cryptomator.frontend.webdav.mount.command.Script; + +@Singleton +final class MacOsXShellScriptWebDavMounter implements WebDavMounterStrategy { + + private final Comparator semVerComparator; + + @Inject + MacOsXShellScriptWebDavMounter(@Named("SemVer") Comparator semVerComparator) { + this.semVerComparator = semVerComparator; + } + + @Override + public boolean shouldWork() { + return SystemUtils.IS_OS_MAC_OSX && semVerComparator.compare(SystemUtils.OS_VERSION, "10.10") < 0; + } + + @Override + public void warmUp(int serverPort) { + // no-op + } + + @Override + public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { + final String mountName = mountParams.getOrDefault(MountParam.MOUNT_NAME, Optional.empty()).orElseThrow(() -> { + return new IllegalArgumentException("Missing mount parameter MOUNT_NAME."); + }); + + // we don't use the uri to derive a path, as it *could* be longer than 255 chars. + final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString(); + final Script mountScript = Script.fromLines("mkdir \"$MOUNT_PATH\"", "mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"").addEnv("DAV_AUTHORITY", uri.getRawAuthority()) + .addEnv("DAV_PATH", uri.getRawPath()).addEnv("MOUNT_PATH", path).addEnv("MOUNT_NAME", mountName); + mountScript.execute(); + return new MacWebDavMount(path); + } + + private static class MacWebDavMount extends AbstractWebDavMount { + private final String mountPath; + private final Script revealScript; + private final Script unmountScript; + + private MacWebDavMount(String mountPath) { + this.mountPath = mountPath; + this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath); + this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath); + } + + @Override + public void unmount() throws CommandFailedException { + // only attempt unmount if user didn't unmount manually: + if (Files.exists(FileSystems.getDefault().getPath(mountPath))) { + unmountScript.execute(); + } + } + + @Override + public void reveal() throws CommandFailedException { + revealScript.execute(); + } + + } + +} diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java index edb5645fa..3cd1b2a08 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/MountStrategies.java @@ -19,74 +19,87 @@ import javax.inject.Singleton; @Singleton class MountStrategies implements Collection { - + private final Collection delegate; - + @Inject - MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) { - delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter)); + MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXAppleScriptWebDavMounter osxAppleScriptMounter, MacOsXShellScriptWebDavMounter osxShellScriptMounter, WindowsWebDavMounter winMounter) { + delegate = unmodifiableList(asList(linuxMounter, osxAppleScriptMounter, osxShellScriptMounter, winMounter)); } + @Override public int size() { return delegate.size(); } + @Override public boolean isEmpty() { return delegate.isEmpty(); } + @Override public boolean contains(Object o) { return delegate.contains(o); } + @Override public Iterator iterator() { return delegate.iterator(); } + @Override public Object[] toArray() { return delegate.toArray(); } + @Override public T[] toArray(T[] a) { return delegate.toArray(a); } + @Override public boolean add(WebDavMounterStrategy e) { return delegate.add(e); } + @Override public boolean remove(Object o) { return delegate.remove(o); } + @Override public boolean containsAll(Collection c) { return delegate.containsAll(c); } + @Override public boolean addAll(Collection c) { return delegate.addAll(c); } + @Override public boolean removeAll(Collection c) { return delegate.removeAll(c); } + @Override public boolean retainAll(Collection c) { return delegate.retainAll(c); } + @Override public void clear() { delegate.clear(); } + @Override public boolean equals(Object o) { return delegate.equals(o); } + @Override public int hashCode() { return delegate.hashCode(); } - - } diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java index a2a257a61..ee8fd5b96 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java @@ -11,10 +11,16 @@ package org.cryptomator.frontend.webdav.mount; import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines; +import java.io.IOException; +import java.io.InterruptedIOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -22,12 +28,16 @@ import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.Frontend.MountParam; import org.cryptomator.frontend.webdav.mount.command.CommandResult; import org.cryptomator.frontend.webdav.mount.command.Script; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A {@link WebDavMounterStrategy} utilizing the "net use" command. @@ -37,7 +47,9 @@ import org.cryptomator.frontend.webdav.mount.command.Script; @Singleton final class WindowsWebDavMounter implements WebDavMounterStrategy { + private static final Logger LOG = LoggerFactory.getLogger(WindowsWebDavMounter.class); private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*"); + private static final Pattern REG_QUERY_PROXY_OVERRIDES_PATTERN = Pattern.compile("\\s*ProxyOverride\\s+REG_SZ\\s+(.*)\\s*"); private static final String AUTO_ASSIGN_DRIVE_LETTER = "*"; private static final String LOCALHOST = "localhost"; private static final int MOUNT_TIMEOUT_SECONDS = 60; @@ -60,12 +72,12 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(URI uri, Map> mountParams) throws CommandFailedException { - final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.of(AUTO_ASSIGN_DRIVE_LETTER)).orElse(AUTO_ASSIGN_DRIVE_LETTER); + final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.empty()).orElse(AUTO_ASSIGN_DRIVE_LETTER); if (driveLetters.getOccupiedDriveLetters().contains(CharUtils.toChar(driveLetter))) { throw new CommandFailedException("Drive letter occupied."); } - - final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.of(LOCALHOST)).orElse(LOCALHOST); + + final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.empty()).orElse(LOCALHOST); try { final URI adjustedUri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); CommandResult mountResult = mount(adjustedUri, driveLetter); @@ -74,14 +86,14 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { throw new IllegalArgumentException("Invalid host: " + hostname); } } - + private CommandResult mount(URI uri, String driveLetter) throws CommandFailedException { - final Script proxyBypassScript = fromLines( - "reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \";%DAV_HOST%;%DAV_HOST%:%DAV_PORT%\" /f"); - proxyBypassScript.addEnv("DAV_HOST", uri.getHost()); - proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); - proxyBypassScript.execute(); - + try { + addProxyOverrides(uri); + } catch (IOException e) { + throw new CommandFailedException(e); + } + final String driveLetterStr = AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? AUTO_ASSIGN_DRIVE_LETTER : driveLetter + ":"; final Script mountScript = fromLines("net use %DRIVE_LETTER% \\\\%DAV_HOST%@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); mountScript.addEnv("DRIVE_LETTER", driveLetterStr); @@ -90,6 +102,44 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } + + private void addProxyOverrides(URI uri) throws IOException, CommandFailedException { + try { + // get existing value for ProxyOverride key from reqistry: + ProcessBuilder query = new ProcessBuilder("reg", "query", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride"); + Process queryCmd = query.start(); + String queryStdOut = IOUtils.toString(queryCmd.getInputStream(), StandardCharsets.UTF_8); + int queryResult = queryCmd.waitFor(); + + // determine new value for ProxyOverride key: + Set overrides = new HashSet<>(); + Matcher matcher = REG_QUERY_PROXY_OVERRIDES_PATTERN.matcher(queryStdOut); + if (queryResult == 0 && matcher.find()) { + String[] existingOverrides = StringUtils.split(matcher.group(1), ';'); + overrides.addAll(Arrays.asList(existingOverrides)); + } + overrides.removeIf(s -> s.startsWith(uri.getHost() + ":")); + overrides.add(""); + overrides.add(uri.getHost()); + overrides.add(uri.getHost() + ":" + uri.getPort()); + + // set new value: + String overridesStr = StringUtils.join(overrides, ';'); + ProcessBuilder add = new ProcessBuilder("reg", "add", "\"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\"", "/v", "ProxyOverride", "/d", "\"" + overridesStr + "\"", "/f"); + LOG.debug("Invoking command: " + StringUtils.join(add.command(), ' ')); + Process addCmd = add.start(); + int addResult = addCmd.waitFor(); + if (addResult != 0) { + String addStdErr = IOUtils.toString(addCmd.getErrorStream(), StandardCharsets.UTF_8); + throw new CommandFailedException(addStdErr); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + InterruptedIOException ioException = new InterruptedIOException(); + ioException.initCause(e); + throw ioException; + } + } private String getDriveLetter(String result) throws CommandFailedException { final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result); diff --git a/main/jacoco-report/.gitignore b/main/jacoco-report/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/main/jacoco-report/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml new file mode 100644 index 000000000..20de8d2bf --- /dev/null +++ b/main/jacoco-report/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + org.cryptomator + main + 1.1.0-SNAPSHOT + + jacoco-report + Cryptomator Code Coverage Report + + + + + org.cryptomator + commons + + + org.cryptomator + commons-test + + + + + org.cryptomator + filesystem-api + + + org.cryptomator + filesystem-charsets + + + org.cryptomator + filesystem-crypto + + + org.cryptomator + filesystem-crypto-integration-tests + + + org.cryptomator + filesystem-inmemory + + + org.cryptomator + filesystem-nameshortening + + + org.cryptomator + filesystem-nio + + + org.cryptomator + filesystem-stats + + + + + org.cryptomator + frontend-api + + + org.cryptomator + frontend-webdav + + + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + diff --git a/main/pom.xml b/main/pom.xml index aebabe97b..75ba59663 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -35,12 +35,12 @@ 1.3 2.4 4.0 - 3.3.2 + 3.4 1.10 3.1 2.4.4 1.10.19 - 2.0.2 + 2.4 @@ -49,6 +49,16 @@ https://jitpack.io + + + + jacoco-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + @@ -70,6 +80,11 @@ filesystem-api ${project.version} + + org.cryptomator + filesystem-charsets + ${project.version} + org.cryptomator filesystem-nio @@ -81,6 +96,12 @@ ${project.version} test + + org.cryptomator + filesystem-invariants-tests + ${project.version} + test + org.cryptomator filesystem-nameshortening @@ -238,7 +259,6 @@ hamcrest-all ${hamcrest.version} - @@ -271,6 +291,7 @@ frontend-api frontend-webdav ui + filesystem-charsets @@ -281,6 +302,12 @@ ant-kit + + test-coverage + + jacoco-report + + @@ -305,7 +332,7 @@ org.jacoco jacoco-maven-plugin - 0.7.5.201505241946 + 0.7.7-SNAPSHOT prepare-agent @@ -314,6 +341,12 @@ + + + **/*_* + **/Dagger* + + @@ -332,6 +365,9 @@ coveralls-maven-plugin 4.0.0 + + jacoco-report/target/site/jacoco-aggregate/jacoco.xml + ${env.COVERALLS_REPO_TOKEN} diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 6808f3887..873c1af38 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -38,6 +38,10 @@ org.cryptomator filesystem-crypto + + org.cryptomator + filesystem-charsets + org.cryptomator filesystem-stats diff --git a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java b/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java index f4fbdb1a0..3150ddc4d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java +++ b/main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java @@ -36,6 +36,8 @@ public class Cryptomator { private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer(); public static void main(String[] args) { + String cryptomatorVersion = Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion()).orElse("SNAPSHOT"); + LOG.info("Starting Cryptomator {} on {} {} ({})", cryptomatorVersion, SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); if (SystemUtils.IS_OS_MAC_OSX) { /* * On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index fa432cc9d..3740b566d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -8,13 +8,13 @@ *******************************************************************************/ package org.cryptomator.ui; -import java.util.Comparator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.inject.Named; import javax.inject.Singleton; +import org.cryptomator.common.CommonsModule; import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.WebDavServer; @@ -24,7 +24,6 @@ import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; import org.cryptomator.ui.util.DeferredCloser; -import org.cryptomator.ui.util.SemVerComparator; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,7 +32,7 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = CryptoEngineModule.class) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class}) class CryptomatorModule { private final Application application; @@ -65,13 +64,6 @@ class CryptomatorModule { return closer; } - @Provides - @Singleton - @Named("SemVer") - Comparator provideSemVerComparator() { - return new SemVerComparator(); - } - @Provides @Singleton @Named("VaultJsonMapper") diff --git a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java index 39cb090e3..3c4976cf4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -136,6 +136,7 @@ class ExitUtil { return; } else { settings.setNumTrayNotifications(settings.getNumTrayNotifications() - 1); + settings.save(); } final Runnable notificationCmd; if (SystemUtils.IS_OS_MAC_OSX) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index ed01184b3..86c16261b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -38,6 +38,7 @@ public class MainApplication extends Application { @Override public void start(Stage primaryStage) throws IOException { + LOG.info("JavaFX application started"); final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this, primaryStage)).build(); final MainController mainCtrl = comp.mainController(); closer = comp.deferredCloser(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 2b5e644c9..5944cf999 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -12,21 +12,11 @@ package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import javax.inject.Inject; import javax.inject.Singleton; -import com.nulabinc.zxcvbn.Strength; -import com.nulabinc.zxcvbn.Zxcvbn; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.scene.control.Label; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.engine.InvalidPassphraseException; import org.cryptomator.crypto.engine.UnsupportedVaultFormatException; import org.cryptomator.ui.controls.SecPasswordField; @@ -40,12 +30,16 @@ import org.slf4j.LoggerFactory; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; import javafx.scene.text.Text; @Singleton @@ -54,19 +48,18 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class); private final Application app; + private final PasswordStrengthUtil strengthRater; final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); - final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 @Inject - public ChangePasswordController(Application app, Localization localization) { + public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) { super(localization); this.app = app; + this.strengthRater = strengthRater; } - @Inject - PasswordStrengthUtil strengthRater; - @FXML private SecPasswordField oldPasswordField; @@ -89,7 +82,19 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private Label passwordStrengthLabel; @FXML - private Rectangle passwordStrengthShape; + private Region passwordStrengthLevel0; + + @FXML + private Region passwordStrengthLevel1; + + @FXML + private Region passwordStrengthLevel2; + + @FXML + private Region passwordStrengthLevel3; + + @FXML + private Region passwordStrengthLevel4; @Override public void initialize() { @@ -99,9 +104,16 @@ public class ChangePasswordController extends LocalizedFXMLViewController { changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate)); - passwordStrengthShape.widthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getWidth)); - passwordStrengthShape.fillProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthColor)); - passwordStrengthShape.strokeWidthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrokeWidth)); + passwordStrengthLevel0.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(0)); + passwordStrengthLevel1.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(1)); + passwordStrengthLevel2.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(2)); + passwordStrengthLevel3.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(3)); + passwordStrengthLevel4.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(4)); + passwordStrengthLevel0.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel1.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel2.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel3.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel4.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 0a87c4fd6..96016aa55 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -9,16 +9,15 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import javafx.application.Platform; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.*; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; -import org.apache.commons.lang3.StringUtils; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.FileAlreadyExistsException; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Singleton; + import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; @@ -27,35 +26,34 @@ import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URL; -import java.nio.file.FileAlreadyExistsException; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import com.nulabinc.zxcvbn.*; +import javafx.application.Platform; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; @Singleton public class InitializeController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); + private final PasswordStrengthUtil strengthRater; final ObjectProperty vault = new SimpleObjectProperty<>(); private Optional listener = Optional.empty(); - final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 @Inject - public InitializeController(Localization localization) { + public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) { super(localization); + this.strengthRater = strengthRater; } - @Inject - PasswordStrengthUtil strengthRater; - @FXML private SecPasswordField passwordField; @@ -72,7 +70,19 @@ public class InitializeController extends LocalizedFXMLViewController { private Label passwordStrengthLabel; @FXML - private Rectangle passwordStrengthShape; + private Region passwordStrengthLevel0; + + @FXML + private Region passwordStrengthLevel1; + + @FXML + private Region passwordStrengthLevel2; + + @FXML + private Region passwordStrengthLevel3; + + @FXML + private Region passwordStrengthLevel4; @Override public void initialize() { @@ -81,9 +91,16 @@ public class InitializeController extends LocalizedFXMLViewController { okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate)); - passwordStrengthShape.widthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getWidth)); - passwordStrengthShape.fillProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthColor)); - passwordStrengthShape.strokeWidthProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrokeWidth)); + passwordStrengthLevel0.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(0)); + passwordStrengthLevel1.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(1)); + passwordStrengthLevel2.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(2)); + passwordStrengthLevel3.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(3)); + passwordStrengthLevel4.visibleProperty().bind(passwordStrength.greaterThanOrEqualTo(4)); + passwordStrengthLevel0.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel1.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel2.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel3.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); + passwordStrengthLevel4.backgroundProperty().bind(EasyBind.map(passwordStrength, strengthRater::getBackgroundWithStrengthColor)); passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 5c262f6de..f316fe2c2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -40,6 +40,7 @@ import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -94,6 +95,9 @@ public class MainController extends LocalizedFXMLViewController { this.changePasswordController = changePasswordController; this.settingsController = settingsController; this.vaults = FXCollections.observableList(settings.getDirectories()); + this.vaults.addListener((Change c) -> { + settings.save(); + }); // derived bindings: this.isShowingSettings = activeController.isEqualTo(settingsController.get()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 7af0e8269..80cd60cab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -59,9 +59,9 @@ public class SettingsController extends LocalizedFXMLViewController { useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); - EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled); + EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), this::checkForUpdateDidChange); EasyBind.subscribe(portField.textProperty(), this::portDidChange); - EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), settings::setUseIpv6); + EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), this::useIpv6DidChange); } @Override @@ -73,21 +73,30 @@ public class SettingsController extends LocalizedFXMLViewController { return Optional.ofNullable(getClass().getPackage().getImplementationVersion()); } + private void checkForUpdateDidChange(Boolean newValue) { + settings.setCheckForUpdatesEnabled(newValue); + settings.save(); + } + private void portDidChange(String newValue) { try { int port = Integer.parseInt(newValue); - if (port < Settings.MIN_PORT) { + if (port < Settings.MIN_PORT || port > Settings.MAX_PORT) { settings.setPort(Settings.DEFAULT_PORT); - } else if (port < Settings.MAX_PORT) { - settings.setPort(port); } else { - portField.setText(String.valueOf(Settings.MAX_PORT)); + settings.setPort(port); + settings.save(); } } catch (NumberFormatException e) { portField.setText(String.valueOf(Settings.DEFAULT_PORT)); } } + private void useIpv6DidChange(Boolean newValue) { + settings.setUseIpv6(newValue); + settings.save(); + } + private void filterNumericKeyEvents(KeyEvent t) { if (t.getCharacter() == null || t.getCharacter().length() == 0) { return; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 376e56d09..3cbc4bbc1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -30,6 +30,7 @@ import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.Optionals; import org.cryptomator.crypto.engine.InvalidPassphraseException; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem; import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate; import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory; import org.cryptomator.filesystem.nio.NioFileSystem; @@ -126,7 +127,8 @@ public class Vault implements CryptoFileSystemDelegate { FileSystem fs = getNioFileSystem(); FileSystem shorteningFs = shorteningFileSystemFactory.get(fs); FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(shorteningFs, passphrase, this); - StatsFileSystem statsFs = new StatsFileSystem(cryptoFs); + FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC); + StatsFileSystem statsFs = new StatsFileSystem(normalizingFs); statsFileSystem = Optional.of(statsFs); String contextPath = StringUtils.prependIfMissing(mountName, "/"); Frontend frontend = frontendFactory.create(statsFs, contextPath); diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java index d12082ef5..1c2fbf8d5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Localization.java @@ -1,27 +1,70 @@ package org.cryptomator.ui.settings; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; +import java.util.Locale; +import java.util.Objects; +import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + @Singleton public class Localization extends ResourceBundle { + private static final Logger LOG = LoggerFactory.getLogger(Localization.class); + + private static final String LOCALIZATION_DEFAULT_FILE = "/localization/en.txt"; + private static final String LOCALIZATION_FILENAME_FMT = "/localization/%s.txt"; + private static final String LOCALIZATION_FILE = String.format(LOCALIZATION_FILENAME_FMT, Locale.getDefault().getLanguage()); + + private final ResourceBundle fallback; + private final ResourceBundle localized; + @Inject public Localization() { - this.parent = ResourceBundle.getBundle("localization"); + try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_DEFAULT_FILE)) { + Objects.requireNonNull(in); + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + this.fallback = new PropertyResourceBundle(reader); + LOG.info("Loaded localization from {}", LOCALIZATION_FILE); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + try (InputStream in = getClass().getResourceAsStream(LOCALIZATION_FILE)) { + if (in != null) { + Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + this.localized = new PropertyResourceBundle(reader); + } else { + this.localized = this.fallback; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override protected Object handleGetObject(String key) { - return parent.getObject(key); + return localized.containsKey(key) ? localized.getObject(key) : fallback.getObject(key); } @Override public Enumeration getKeys() { - return parent.getKeys(); + Collection keys = CollectionUtils.union(localized.keySet(), fallback.keySet()); + return Collections.enumeration(keys); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 85c8fefdc..17f7c5b25 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -11,6 +11,7 @@ package org.cryptomator.ui.settings; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.cryptomator.ui.model.Vault; @@ -23,10 +24,12 @@ public class Settings implements Serializable { private static final long serialVersionUID = 7609959894417878744L; public static final int MIN_PORT = 1024; public static final int MAX_PORT = 65535; - public static final int DEFAULT_PORT = 0; + public static final int DEFAULT_PORT = 42427; public static final boolean DEFAULT_USE_IPV6 = false; public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; + private final Consumer saveCmd; + @JsonProperty("directories") private List directories; @@ -35,7 +38,7 @@ public class Settings implements Serializable { @JsonProperty("port") private Integer port; - + @JsonProperty("useIpv6") private Boolean useIpv6; @@ -45,8 +48,12 @@ public class Settings implements Serializable { /** * Package-private constructor; use {@link SettingsProvider}. */ - Settings() { + Settings(Consumer saveCmd) { + this.saveCmd = saveCmd; + } + public void save() { + saveCmd.accept(this); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java index 9d40ac09d..417b6c369 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/SettingsProvider.java @@ -16,6 +16,12 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import javax.inject.Named; @@ -23,7 +29,6 @@ import javax.inject.Provider; import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.ui.util.DeferredCloser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +37,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; @Singleton public class SettingsProvider implements Provider { - private static final Logger LOG = LoggerFactory.getLogger(Settings.class); + private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class); private static final Path SETTINGS_DIR; private static final String SETTINGS_FILE = "settings.json"; + private static final long SAVE_DELAY_MS = 1000; static { final String appdata = System.getenv("APPDATA"); @@ -52,12 +58,12 @@ public class SettingsProvider implements Provider { } } - private final DeferredCloser deferredCloser; private final ObjectMapper objectMapper; + private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicReference> scheduledSaveCmd = new AtomicReference<>(); @Inject - public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) { - this.deferredCloser = deferredCloser; + public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @@ -72,28 +78,39 @@ public class SettingsProvider implements Provider { @Override public Settings get() { - Settings settings = null; + final Settings settings = new Settings(this::scheduleSave); try { final Path settingsPath = getSettingsPath(); final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); - settings = objectMapper.readValue(in, Settings.class); + objectMapper.readerForUpdating(settings).readValue(in); + LOG.info("Settings loaded from " + settingsPath); } catch (IOException e) { - LOG.warn("Failed to load settings, creating new one."); - settings = new Settings(); + LOG.info("Failed to load settings, creating new one."); } - deferredCloser.closeLater(settings, this::save); return settings; } - private void save(Settings 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); + } + } + + private void save(Settings settings) { + Objects.requireNonNull(settings); try { final Path settingsPath = getSettingsPath(); Files.createDirectories(settingsPath.getParent()); final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); objectMapper.writeValue(out, settings); + LOG.info("Settings saved to " + settingsPath); } catch (IOException e) { LOG.error("Failed to save settings.", e); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java index fe6af2f61..b2871a9b3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java @@ -8,105 +8,76 @@ *******************************************************************************/ package org.cryptomator.ui.util; -import com.nulabinc.zxcvbn.Zxcvbn; -import javafx.beans.property.IntegerProperty; -import javafx.scene.paint.Color; -import org.apache.commons.lang3.StringUtils; -import org.cryptomator.ui.settings.Localization; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.ArrayList; -import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.ui.settings.Localization; + +import com.nulabinc.zxcvbn.Zxcvbn; + +import javafx.geometry.Insets; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.paint.Color; @Singleton public class PasswordStrengthUtil { - private final Zxcvbn zxcvbn; - private final List sanitizedInputs; - private final Localization localization; + private final Zxcvbn zxcvbn; + private final List sanitizedInputs; + private final Localization localization; - @Inject - public PasswordStrengthUtil(Localization localization){ - this.localization = localization; - this.zxcvbn = new Zxcvbn(); - this.sanitizedInputs = new ArrayList<>(); - this.sanitizedInputs.add("cryptomator"); - } + @Inject + public PasswordStrengthUtil(Localization localization) { + this.localization = localization; + this.zxcvbn = new Zxcvbn(); + this.sanitizedInputs = new ArrayList<>(); + this.sanitizedInputs.add("cryptomator"); + } - public int computeRate(String password) { - if (StringUtils.isEmpty(password)) { - return -1; - } else { - return zxcvbn.measure(password, sanitizedInputs).getScore(); - } - } + public int computeRate(String password) { + if (StringUtils.isEmpty(password)) { + return -1; + } else { + return zxcvbn.measure(password, sanitizedInputs).getScore(); + } + } - public Color getStrengthColor(Number score) { - Color strengthColor = Color.web("#FF0000"); - switch (score.intValue()) { - case 0: - strengthColor = Color.web("#FF0000"); - break; - case 1: - strengthColor = Color.web("#FF8000"); - break; - case 2: - strengthColor = Color.web("#FFBF00"); - break; - case 3: - strengthColor = Color.web("#FFFF00"); - break; - case 4: - strengthColor = Color.web("#BFFF00"); - break; - default: - strengthColor = Color.web("#FF0000"); - break; - } - return strengthColor; - } + public Color getStrengthColor(Number score) { + switch (score.intValue()) { + case 0: + return Color.web("#e74c3c"); + case 1: + return Color.web("#e67e22"); + case 2: + return Color.web("#f1c40f"); + case 3: + return Color.web("#40d47e"); + case 4: + return Color.web("#27ae60"); + default: + return Color.TRANSPARENT; + } + } - public int getWidth(Number score) { - int width = 0; - switch (score.intValue()) { - case 0: - width += 5; - break; - case 1: - width += 25; - break; - case 2: - width += 50; - break; - case 3: - width += 75; - break; - case 4: - width = 100; - break; - default: - width = 0; - break; - } - return Math.round(width*2.23f); - } + public Background getBackgroundWithStrengthColor(Number score) { + Color c = this.getStrengthColor(score); + BackgroundFill fill = new BackgroundFill(c, CornerRadii.EMPTY, Insets.EMPTY); + return new Background(fill); + } - public float getStrokeWidth(Number score) { - if (score.intValue() >= 0) { - return 0.5f; - } else { - return 0; - } - } - - public String getStrengthDescription(Number score) { - if (score.intValue() >= 0) { - return String.format(localization.getString("initialize.messageLabel.passwordStrength"), - localization.getString("initialize.messageLabel.passwordStrength." + score.intValue())); - } else { - return ""; - } - } + public String getStrengthDescription(Number score) { + String key = "initialize.messageLabel.passwordStrength." + score.intValue(); + if (localization.containsKey(key)) { + return localization.getString(key); + } else { + return ""; + } + } } diff --git a/main/ui/src/main/resources/bot_welcome.png b/main/ui/src/main/resources/bot_welcome.png index 5daf92fd6..a429621d9 100644 Binary files a/main/ui/src/main/resources/bot_welcome.png and b/main/ui/src/main/resources/bot_welcome.png differ diff --git a/main/ui/src/main/resources/bot_welcome@2x.png b/main/ui/src/main/resources/bot_welcome@2x.png index 8c25a5e21..d6e201c20 100644 Binary files a/main/ui/src/main/resources/bot_welcome@2x.png and b/main/ui/src/main/resources/bot_welcome@2x.png differ diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index 4beabf9e8..9c8fc2f3a 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -50,6 +50,10 @@ -fx-font-family: Ionicons; } +.caption-label { + -fx-font-size: 0.9em; +} + /**************************************************************************** * * * Hyperlinks * diff --git a/main/ui/src/main/resources/css/mac_theme.css b/main/ui/src/main/resources/css/mac_theme.css index bac4a985d..33d1e223e 100644 --- a/main/ui/src/main/resources/css/mac_theme.css +++ b/main/ui/src/main/resources/css/mac_theme.css @@ -49,6 +49,10 @@ -fx-font-family: Ionicons; } +.caption-label { + -fx-font-size: 0.9em; +} + /**************************************************************************** * * * Hyperlinks * diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index f0109b004..bfd569b54 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -42,6 +42,10 @@ -fx-font-family: Ionicons; } +.caption-label { + -fx-font-size: 0.9em; +} + /**************************************************************************** * * * Hyperlinks * diff --git a/main/ui/src/main/resources/fxml/change_password.fxml b/main/ui/src/main/resources/fxml/change_password.fxml index 22c17ce9b..44d985526 100644 --- a/main/ui/src/main/resources/fxml/change_password.fxml +++ b/main/ui/src/main/resources/fxml/change_password.fxml @@ -21,9 +21,10 @@ + + + - - @@ -46,18 +47,24 @@