mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-19 17:16:53 -04:00
Merge remote-tracking branch 'refs/remotes/cryptomator/master'
This commit is contained in:
@@ -14,6 +14,12 @@
|
||||
</includes>
|
||||
<outputDirectory>libs</outputDirectory>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/fixed-binaries</directory>
|
||||
<filtered>false</filtered>
|
||||
<outputDirectory>fixed-binaries</outputDirectory>
|
||||
<fileMode>755</fileMode>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>target/package</directory>
|
||||
<filtered>false</filtered>
|
||||
|
||||
@@ -60,6 +60,16 @@
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<excludes>
|
||||
<exclude>fixed-binaries/**</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<includes>
|
||||
<include>fixed-binaries/**</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<fx:resources>
|
||||
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
|
||||
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
|
||||
<fx:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
50
main/ant-kit/src/main/resources/package/linux/postinst
Normal file
50
main/ant-kit/src/main/resources/package/linux/postinst
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# postinst script for APPLICATION_NAME
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
# summary of how this script can be called:
|
||||
# * <postinst> `configure' <most-recently-configured-version>
|
||||
# * <old-postinst> `abort-upgrade' <new version>
|
||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||
# <new-version>
|
||||
# * <postinst> `abort-remove'
|
||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||
# <failed-install-package> <version> `removing'
|
||||
# <conflicting-package> <version>
|
||||
# for details, see http://www.debian.org/doc/debian-policy/ or
|
||||
# the debian-policy package
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
echo Adding shortcut to the menu
|
||||
SECONDARY_LAUNCHERS_INSTALL
|
||||
APP_CDS_CACHE
|
||||
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
|
||||
FILE_ASSOCIATION_INSTALL
|
||||
|
||||
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
if [ $(uname -m) = "x86_64" ]; then
|
||||
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
else
|
||||
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
fi
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# dh_installdeb will replace this with shell code automatically
|
||||
# generated by other debhelper scripts.
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
54
main/ant-kit/src/main/resources/package/linux/spec
Normal file
54
main/ant-kit/src/main/resources/package/linux/spec
Normal file
@@ -0,0 +1,54 @@
|
||||
Summary: APPLICATION_SUMMARY
|
||||
Name: APPLICATION_PACKAGE
|
||||
Version: APPLICATION_VERSION
|
||||
Release: 1
|
||||
License: APPLICATION_LICENSE_TYPE
|
||||
Vendor: APPLICATION_VENDOR
|
||||
Prefix: /opt
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
|
||||
#avoid ARCH subfolder
|
||||
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
%description
|
||||
APPLICATION_DESCRIPTION
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}/opt
|
||||
cp -r %{_sourcedir}/APPLICATION_FS_NAME %{buildroot}/opt
|
||||
|
||||
%files
|
||||
APPLICATION_LICENSE_FILE
|
||||
/opt/APPLICATION_FS_NAME
|
||||
|
||||
%post
|
||||
SECONDARY_LAUNCHERS_INSTALL
|
||||
APP_CDS_CACHE
|
||||
xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
|
||||
FILE_ASSOCIATION_INSTALL
|
||||
rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
if [ $(uname -m) = "x86_64" ]; then
|
||||
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
else
|
||||
mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME
|
||||
fi
|
||||
|
||||
%preun
|
||||
SECONDARY_LAUNCHERS_REMOVE
|
||||
xdg-desktop-menu uninstall --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
|
||||
FILE_ASSOCIATION_REMOVE
|
||||
|
||||
%clean
|
||||
@@ -8,7 +8,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
abstract class CryptoException extends RuntimeException {
|
||||
public abstract class CryptoException extends RuntimeException {
|
||||
|
||||
public CryptoException() {
|
||||
super();
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
|
||||
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
|
||||
@@ -22,12 +24,9 @@ public interface FilenameCryptor {
|
||||
String hashDirectoryId(String cleartextDirectoryId);
|
||||
|
||||
/**
|
||||
* Tests without an actual decryption attempt, if a name is a well-formed ciphertext.
|
||||
*
|
||||
* @param ciphertextName Filename in question
|
||||
* @return <code>true</code> if the given name is likely to be a valid ciphertext
|
||||
* @return A Pattern that can be used to test, if a name is a well-formed ciphertext.
|
||||
*/
|
||||
boolean isEncryptedFilename(String ciphertextName);
|
||||
Pattern encryptedNamePattern();
|
||||
|
||||
/**
|
||||
* @param cleartextName original filename including cleartext file extension
|
||||
|
||||
@@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -25,6 +26,7 @@ import org.cryptomator.siv.SivMode;
|
||||
class FilenameCryptorImpl implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||
@Override
|
||||
@@ -50,8 +52,8 @@ class FilenameCryptorImpl implements FilenameCryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncryptedFilename(String ciphertextName) {
|
||||
return BASE32.isInAlphabet(ciphertextName);
|
||||
public Pattern encryptedNamePattern() {
|
||||
return BASE32_PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class ConflictResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||
|
||||
private final Pattern encryptedNamePattern;
|
||||
private final Function<String, Optional<String>> nameDecryptor;
|
||||
private final Function<String, Optional<String>> nameEncryptor;
|
||||
|
||||
public ConflictResolver(Pattern encryptedNamePattern, Function<String, Optional<String>> nameDecryptor, Function<String, Optional<String>> nameEncryptor) {
|
||||
this.encryptedNamePattern = encryptedNamePattern;
|
||||
this.nameDecryptor = nameDecryptor;
|
||||
this.nameEncryptor = nameEncryptor;
|
||||
}
|
||||
|
||||
public File resolveIfNecessary(File file) {
|
||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeEnd(file.name(), DIR_SUFFIX));
|
||||
if (m.matches()) {
|
||||
// full match, use file as is
|
||||
return file;
|
||||
} else if (m.find(0)) {
|
||||
// partial match, might be conflicting
|
||||
return resolveConflict(file, m.toMatchResult());
|
||||
} else {
|
||||
// no match, file not relevant
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
||||
String ciphertext = matchResult.group();
|
||||
boolean isDirectory = conflictingFile.name().substring(matchResult.end()).startsWith(DIR_SUFFIX);
|
||||
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
||||
if (cleartext.isPresent()) {
|
||||
Folder folder = conflictingFile.parent().get();
|
||||
File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext);
|
||||
if (canonicalFile.exists()) {
|
||||
// conflict detected! look for an alternative name:
|
||||
File alternativeFile;
|
||||
String conflictId;
|
||||
do {
|
||||
conflictId = createConflictId();
|
||||
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
||||
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
||||
alternativeFile = folder.file(isDirectory ? alternativeCiphertext + DIR_SUFFIX : alternativeCiphertext);
|
||||
} while (alternativeFile.exists());
|
||||
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||
conflictingFile.moveTo(alternativeFile);
|
||||
return alternativeFile;
|
||||
} else {
|
||||
conflictingFile.moveTo(canonicalFile);
|
||||
return canonicalFile;
|
||||
}
|
||||
} else {
|
||||
// not decryptable; false positive
|
||||
return conflictingFile;
|
||||
}
|
||||
}
|
||||
|
||||
private String createConflictId() {
|
||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.util.Optional;
|
||||
@@ -27,9 +25,7 @@ class CryptoFile extends CryptoNode implements File {
|
||||
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId);
|
||||
});
|
||||
return parent().get().encryptChildName(name());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,35 +18,42 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.common.WeakValuedCache;
|
||||
import org.cryptomator.common.streams.AutoClosingStream;
|
||||
import org.cryptomator.crypto.engine.CryptoException;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.Deleter;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.io.FileContents;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class CryptoFolder extends CryptoNode implements Folder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFolder.class);
|
||||
private final WeakValuedCache<String, CryptoFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
|
||||
private final WeakValuedCache<String, CryptoFile> files = WeakValuedCache.usingLoader(this::newFile);
|
||||
private final AtomicReference<String> directoryId = new AtomicReference<>();
|
||||
private final ConflictResolver conflictResolver;
|
||||
|
||||
public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
super(parent, name, cryptor);
|
||||
this.conflictResolver = new ConflictResolver(cryptor.getFilenameCryptor().encryptedNamePattern(), this::decryptChildName, this::encryptChildName);
|
||||
}
|
||||
|
||||
/* ======================= name + directory id ======================= */
|
||||
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
if (parent().isPresent()) {
|
||||
return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_SUFFIX;
|
||||
});
|
||||
return parent().get().encryptChildName(name()).map(s -> s + DIR_SUFFIX);
|
||||
} else {
|
||||
return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
|
||||
}
|
||||
@@ -73,24 +80,56 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
}));
|
||||
}
|
||||
|
||||
/* ======================= children ======================= */
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() {
|
||||
return AutoClosingStream.from(Stream.concat(files(), folders()));
|
||||
}
|
||||
|
||||
private Stream<File> nonConflictingFiles() {
|
||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||
return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary);
|
||||
}
|
||||
|
||||
private Predicate<File> containsEncryptedName() {
|
||||
final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern();
|
||||
return (File file) -> encryptedNamePattern.matcher(file.name()).find();
|
||||
}
|
||||
|
||||
Optional<String> decryptChildName(String ciphertextFileName) {
|
||||
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
|
||||
try {
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertextFileName, dirId);
|
||||
} catch (CryptoException e) {
|
||||
LOG.warn("Filename decryption of {} failed: {}", ciphertextFileName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Optional<String> encryptChildName(String cleartextFileName) {
|
||||
return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> {
|
||||
return cryptor.getFilenameCryptor().encryptFilename(cleartextFileName, dirId);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFile> files() {
|
||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||
return files.map(File::name).filter(isEncryptedFileName()).map(this::decryptChildFileName).map(this::file);
|
||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
|
||||
}
|
||||
|
||||
private Predicate<String> isEncryptedFileName() {
|
||||
return (String name) -> !name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(name);
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix()).map(this::removeDirSuffix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
|
||||
}
|
||||
|
||||
private String decryptChildFileName(String encryptedFileName) {
|
||||
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(encryptedFileName, dirId);
|
||||
private Predicate<String> endsWithDirSuffix() {
|
||||
return (String encryptedFolderName) -> StringUtils.endsWith(encryptedFolderName, DIR_SUFFIX);
|
||||
}
|
||||
|
||||
private String removeDirSuffix(String encryptedFolderName) {
|
||||
return StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,35 +137,21 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
return files.get(name);
|
||||
}
|
||||
|
||||
public CryptoFile newFile(String name) {
|
||||
return new CryptoFile(this, name, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<CryptoFolder> folders() {
|
||||
final Stream<? extends File> files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty());
|
||||
return files.map(File::name).filter(isEncryptedDirectoryName()).map(this::decryptChildFolderName).map(this::folder);
|
||||
}
|
||||
|
||||
private Predicate<String> isEncryptedDirectoryName() {
|
||||
return (String name) -> name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(StringUtils.removeEnd(name, DIR_SUFFIX));
|
||||
}
|
||||
|
||||
private String decryptChildFolderName(String encryptedFolderName) {
|
||||
final byte[] dirId = getDirectoryId().get().getBytes(UTF_8);
|
||||
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
|
||||
return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoFolder folder(String name) {
|
||||
return folders.get(name);
|
||||
}
|
||||
|
||||
public CryptoFolder newFolder(String name) {
|
||||
private CryptoFile newFile(String name) {
|
||||
return new CryptoFile(this, name, cryptor);
|
||||
}
|
||||
|
||||
private CryptoFolder newFolder(String name) {
|
||||
return new CryptoFolder(this, name, cryptor);
|
||||
}
|
||||
|
||||
/* ======================= create/move/delete ======================= */
|
||||
|
||||
@Override
|
||||
public void create() {
|
||||
parent.create();
|
||||
@@ -176,7 +201,7 @@ class CryptoFolder extends CryptoNode implements Folder {
|
||||
// cut all ties:
|
||||
this.invalidateDirectoryIdsRecursively();
|
||||
|
||||
assert!exists();
|
||||
assert !exists();
|
||||
assert target.exists();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
@@ -19,6 +20,7 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
||||
class NoFilenameCryptor implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
private static final Pattern WILDCARD_PATTERN = Pattern.compile(".*");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
|
||||
@Override
|
||||
@@ -29,8 +31,8 @@ class NoFilenameCryptor implements FilenameCryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncryptedFilename(String ciphertextName) {
|
||||
return true;
|
||||
public Pattern encryptedNamePattern() {
|
||||
return WILDCARD_PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
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 ConflictResolverTest {
|
||||
|
||||
private ConflictResolver conflictResolver;
|
||||
private Folder folder;
|
||||
private File canonicalFile;
|
||||
private File canonicalFolder;
|
||||
private File conflictingFile;
|
||||
private File conflictingFolder;
|
||||
private File resolved;
|
||||
private File unrelatedFile;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Pattern base32Pattern = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
|
||||
BaseNCodec base32 = new Base32();
|
||||
Function<String, Optional<String>> decode = (s) -> Optional.of(new String(base32.decode(s), StandardCharsets.UTF_8));
|
||||
Function<String, Optional<String>> encode = (s) -> Optional.of(base32.encodeAsString(s.getBytes(StandardCharsets.UTF_8)));
|
||||
conflictResolver = new ConflictResolver(base32Pattern, decode, encode);
|
||||
|
||||
folder = Mockito.mock(Folder.class);
|
||||
canonicalFile = Mockito.mock(File.class);
|
||||
canonicalFolder = Mockito.mock(File.class);
|
||||
conflictingFile = Mockito.mock(File.class);
|
||||
conflictingFolder = Mockito.mock(File.class);
|
||||
resolved = Mockito.mock(File.class);
|
||||
unrelatedFile = Mockito.mock(File.class);
|
||||
|
||||
String canonicalFileName = encode.apply("test name").get();
|
||||
String canonicalFolderName = canonicalFileName + Constants.DIR_SUFFIX;
|
||||
String conflictingFileName = canonicalFileName + " (version 2)";
|
||||
String conflictingFolderName = canonicalFolderName + " (version 2)";
|
||||
String unrelatedName = "notBa$e32!";
|
||||
|
||||
Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName);
|
||||
Mockito.when(canonicalFolder.name()).thenReturn(canonicalFolderName);
|
||||
Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName);
|
||||
Mockito.when(conflictingFolder.name()).thenReturn(conflictingFolderName);
|
||||
Mockito.when(unrelatedFile.name()).thenReturn(unrelatedName);
|
||||
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(true);
|
||||
Mockito.when(canonicalFolder.exists()).thenReturn(true);
|
||||
Mockito.when(conflictingFile.exists()).thenReturn(true);
|
||||
Mockito.when(conflictingFolder.exists()).thenReturn(true);
|
||||
Mockito.when(unrelatedFile.exists()).thenReturn(true);
|
||||
|
||||
Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(canonicalFolder).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(conflictingFolder).parent();
|
||||
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent();
|
||||
|
||||
Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved);
|
||||
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
|
||||
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
|
||||
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
|
||||
Mockito.when(folder.file(conflictingFolderName)).thenReturn(conflictingFolder);
|
||||
Mockito.when(folder.file(unrelatedName)).thenReturn(unrelatedFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanonicalName() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnrelatedName() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(unrelatedFile);
|
||||
Assert.assertSame(unrelatedFile, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFile() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(resolved);
|
||||
Assert.assertSame(resolved, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFileIfCanonicalDoesnExist() {
|
||||
Mockito.when(canonicalFile.exists()).thenReturn(false);
|
||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFile);
|
||||
Mockito.verify(conflictingFile).moveTo(canonicalFile);
|
||||
Assert.assertSame(canonicalFile, resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConflictingFolder() {
|
||||
File resolved = conflictResolver.resolveIfNecessary(conflictingFolder);
|
||||
Mockito.verify(conflictingFolder).moveTo(resolved);
|
||||
Assert.assertSame(resolved, resolved);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -19,9 +17,12 @@ import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.io.FileContents;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class FilenameShortener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FilenameShortener.class);
|
||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
@@ -71,7 +72,8 @@ class FilenameShortener {
|
||||
private String loadMapping(String shortName) {
|
||||
final File mappingFile = mappingFile(shortName);
|
||||
if (!mappingFile.exists()) {
|
||||
throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
|
||||
LOG.warn("Mapping file not found: " + mappingFile);
|
||||
return shortName;
|
||||
} else {
|
||||
return FileContents.UTF_8.readContents(mappingFile);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.shortening;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
@@ -45,12 +43,12 @@ public class FilenameShortenerTest {
|
||||
Assert.assertEquals("short", shortener.inflate("short"));
|
||||
}
|
||||
|
||||
@Test(expected = UncheckedIOException.class)
|
||||
@Test
|
||||
public void testInflateWithoutMappingFile() {
|
||||
FileSystem fs = new InMemoryFileSystem();
|
||||
FilenameShortener shortener = new FilenameShortener(fs, 10);
|
||||
|
||||
shortener.inflate("iJustMadeThisNameUp.lng");
|
||||
Assert.assertEquals("iJustMadeThisNameUp.lng", shortener.inflate("iJustMadeThisNameUp.lng"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -102,6 +103,31 @@ public class ShorteningFileSystemTest {
|
||||
Assert.assertTrue(correspondingMetadataFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInflate() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final Folder metadataRoot = underlyingFs.folder(METADATA_DIR_NAME);
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
|
||||
final File correspondingMetadataFile = metadataRoot.folder("QM").folder("JL").file("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
shortenedFolder.create();
|
||||
correspondingMetadataFile.parent().get().create();
|
||||
try (WritableFile w = correspondingMetadataFile.openWritable()) {
|
||||
w.write(ByteBuffer.wrap("morethantenchars".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
Assert.assertTrue(correspondingMetadataFile.exists());
|
||||
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("morethantenchars")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInflateFailedDueToMissingMapping() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10);
|
||||
final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
|
||||
shortenedFolder.create();
|
||||
Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveLongFolders() {
|
||||
final FileSystem underlyingFs = new InMemoryFileSystem();
|
||||
|
||||
22
main/filesystem-nameshortening/src/test/resources/log4j2.xml
Normal file
22
main/filesystem-nameshortening/src/test/resources/log4j2.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -43,6 +43,9 @@ public class SettingsController extends LocalizedFXMLViewController {
|
||||
@FXML
|
||||
private TextField portField;
|
||||
|
||||
@FXML
|
||||
private Label useIpv6Label;
|
||||
|
||||
@FXML
|
||||
private CheckBox useIpv6Checkbox;
|
||||
|
||||
@@ -55,7 +58,8 @@ public class SettingsController extends LocalizedFXMLViewController {
|
||||
checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled() && !areUpdatesManagedExternally());
|
||||
portField.setText(String.valueOf(settings.getPort()));
|
||||
portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents);
|
||||
useIpv6Checkbox.setDisable(!SystemUtils.IS_OS_WINDOWS);
|
||||
useIpv6Label.setVisible(SystemUtils.IS_OS_WINDOWS);
|
||||
useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS);
|
||||
useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6());
|
||||
versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT")));
|
||||
|
||||
@@ -81,7 +85,7 @@ public class SettingsController extends LocalizedFXMLViewController {
|
||||
private void portDidChange(String newValue) {
|
||||
try {
|
||||
int port = Integer.parseInt(newValue);
|
||||
if (port < Settings.MIN_PORT || port > Settings.MAX_PORT) {
|
||||
if (!settings.isPortValid(port)) {
|
||||
settings.setPort(Settings.DEFAULT_PORT);
|
||||
} else {
|
||||
settings.setPort(port);
|
||||
|
||||
@@ -93,8 +93,8 @@ public class Settings implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPortValid(int port) {
|
||||
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT;
|
||||
public boolean isPortValid(int port) {
|
||||
return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT || port == 0;
|
||||
}
|
||||
|
||||
public boolean shouldUseIpv6() {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<TextField GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="portField" cacheShape="true" cache="true" promptText="%settings.port.prompt" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%settings.useipv6.label" cacheShape="true" cache="true" />
|
||||
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" fx:id="useIpv6Label" text="%settings.useipv6.label" cacheShape="true" cache="true" />
|
||||
<CheckBox GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="useIpv6Checkbox" cacheShape="true" cache="true" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# See the LICENSE.txt file for more info.
|
||||
#
|
||||
# Contributors:
|
||||
# Christian Schmickler
|
||||
# Joscha Feth
|
||||
# Markus Kreusch
|
||||
# Michael Schmetter
|
||||
# Sebastian Wiesendahl
|
||||
@@ -86,3 +88,8 @@ tray.menu.quit = Beenden
|
||||
tray.infoMsg.title = Cryptomator läuft noch
|
||||
tray.infoMsg.msg = Cryptomator läuft noch. Mit dem Tray-Icon beenden.
|
||||
tray.infoMsg.msg.osx = Cryptomator läuft noch. Über die Menüleiste beenden.
|
||||
initialize.messageLabel.passwordStrength.0 = Sehr schwach
|
||||
initialize.messageLabel.passwordStrength.1 = Schwach
|
||||
initialize.messageLabel.passwordStrength.2 = Mittel
|
||||
initialize.messageLabel.passwordStrength.3 = Stark
|
||||
initialize.messageLabel.passwordStrength.4 = Sehr stark
|
||||
@@ -82,3 +82,8 @@ tray.menu.quit = 종료
|
||||
tray.infoMsg.title = 계속 실행 중입니다.
|
||||
tray.infoMsg.msg = Cryptomator가 계속 실행 중입니다. 종료하실려면 트레이 아이콘에서 해주세요.
|
||||
tray.infoMsg.msg.osx = Cryptomator가 계속 실행중입니다. 종료하실려면 메뉴 바 아이콘에서 해주세요.
|
||||
initialize.messageLabel.passwordStrength.0 = 너무 약함
|
||||
initialize.messageLabel.passwordStrength.1 = 약함
|
||||
initialize.messageLabel.passwordStrength.2 = 괜찮음
|
||||
initialize.messageLabel.passwordStrength.3 = 강력함
|
||||
initialize.messageLabel.passwordStrength.4 = 매우 강력함
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
# Filip Havrlent
|
||||
# Tatiana Chovancová
|
||||
|
||||
# Copyright (c) 2016 The Cryptomator Contributors
|
||||
# This file is licensed under the terms of the MIT license.
|
||||
# See the LICENSE.txt file for more info.
|
||||
app.name = Cryptomator
|
||||
# main.fxml
|
||||
main.emptyListInstructions = Pridať trezor
|
||||
|
||||
89
main/ui/src/main/resources/localization/tr.txt
Normal file
89
main/ui/src/main/resources/localization/tr.txt
Normal file
@@ -0,0 +1,89 @@
|
||||
# Copyright (c) 2016 The Cryptomator Contributors
|
||||
# This file is licensed under the terms of the MIT license.
|
||||
# See the LICENSE.txt file for more info.
|
||||
#
|
||||
# Contributors:
|
||||
# Cem KOÇ
|
||||
|
||||
app.name = Cryptomator
|
||||
# main.fxml
|
||||
main.emptyListInstructions = Kasa eklemek için tıkla
|
||||
main.directoryList.contextMenu.remove = Listeden sil
|
||||
main.directoryList.contextMenu.changePassword = Şifreyi değiştir
|
||||
main.addDirectory.contextMenu.new = Yeni bir kasa yarat
|
||||
main.addDirectory.contextMenu.open = Var olan kasayı aç
|
||||
# welcome.fxml
|
||||
welcome.checkForUpdates.label.currentlyChecking = Güncellemeler kontrol ediliyor...
|
||||
welcome.newVersionMessage = Sürüm %s indirilebilir. Şu anki sürüm\: %s
|
||||
# initialize.fxml
|
||||
initialize.label.password = Şifre
|
||||
initialize.label.retypePassword = Şifre (tekrar)
|
||||
initialize.button.ok = Kasa oluştur
|
||||
initialize.messageLabel.alreadyInitialized = Kasa çoktan başlatıldı
|
||||
initialize.messageLabel.initializationFailed = Kasa başlatılamadı. Detaylar için log dosyasına bakın.
|
||||
# notfound.fxml
|
||||
notfound.label = Kasa bulunamadı. Yeri değişmiş olabilir mi ?
|
||||
# upgrade.fxml
|
||||
upgrade.button = Kasayı yükselt.
|
||||
upgrade.version3dropBundleExtension.msg = Bu kasanın yeni formata geçirilmesi gerekmekte. "%1$s" ismi "%2$s" olarak değiştirilecek. Devam etmeden önce senkronizasyonun bittiğine emin olun.
|
||||
upgrade.version3dropBundleExtension.err.alreadyExists = Otomatik format değiştirme sırasında hata. "%s" zaten bulunmakta.
|
||||
# unlock.fxml
|
||||
unlock.label.password = Şifre
|
||||
unlock.label.mountName = Sürücü ismi
|
||||
unlock.label.winDriveLetter = Sürücü konumu
|
||||
unlock.label.downloadsPageLink = Tüm Cryptomator sürümleri
|
||||
unlock.label.advancedHeading = Gelişmiş seçenekler
|
||||
unlock.button.unlock = Kasayı aç
|
||||
unlock.button.advancedOptions.show = Daha fazla seçenek
|
||||
unlock.button.advancedOptions.hide = Daha az seçenek
|
||||
unlock.choicebox.winDriveLetter.auto = Otomatik ata
|
||||
unlock.errorMessage.wrongPassword = Yanlış şifre
|
||||
unlock.errorMessage.mountingFailed = Başlama başarısız. Detaylar için log dosyasına bakın
|
||||
unlock.errorMessage.unsupportedKeyLengthInstallJCE = Şifre çözme başarısız. Lütfen Oracle JCE Unlimited Strength Policy yükleyin.
|
||||
unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware = Desteklenmeyen kasa. Bu kasa daha eski bir Cryptomator sürümü ile oluşturulmuş.
|
||||
unlock.errorMessage.unsupportedVersion.softwareOlderThanVault = Desteklenmeyen kasa. Bu kasa daha yeni bir Cryptomator sürümü ile oluşturulmuş.
|
||||
unlock.messageLabel.startServerFailed = WebDAV sunucu başlatma başarısız.
|
||||
# change_password.fxml
|
||||
changePassword.label.oldPassword = Eski şifre
|
||||
changePassword.label.newPassword = Yeni şifre
|
||||
changePassword.label.retypePassword = Yeni şifre (tekrar)
|
||||
changePassword.label.downloadsPageLink = Tüm Cryptomator sürümleri
|
||||
changePassword.button.change = Şifreyi değiştir
|
||||
changePassword.errorMessage.wrongPassword = Yanlış şifre
|
||||
changePassword.errorMessage.decryptionFailed = Şifre çözme başarısız
|
||||
changePassword.errorMessage.unsupportedKeyLengthInstallJCE = Şifre çözme başarısız. Lütfen Oracle JCE Unlimited Strength Policy yükleyin.
|
||||
changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware = Desteklenmeyen kasa. Bu kasa daha eski bir Cryptomator sürümü ile oluşturulmuş.
|
||||
changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault = Desteklenmeyen kasa. Bu kasa daha yeni bir Cryptomator sürümü ile oluşturulmuş.
|
||||
changePassword.infoMessage.success = Şifre değiştirildi.
|
||||
# unlocked.fxml
|
||||
unlocked.button.lock = Kasayı kilitle
|
||||
unlocked.moreOptions.reveal = Sürücüyü göster
|
||||
unlocked.moreOptions.copyUrl = WebDAV URl'sini kopyala
|
||||
unlocked.label.revealFailed = Komut başarısız
|
||||
unlocked.label.unmountFailed = Sürücü çıkarma başarısız
|
||||
unlocked.label.statsEncrypted = şifrelenmiş
|
||||
unlocked.label.statsDecrypted = şifresi çözülmüş
|
||||
unlocked.ioGraph.yAxis.label = Veri hacmi (MiB/s)
|
||||
# mac_warnings.fxml
|
||||
macWarnings.windowTitle = Tehlike - Bozuk dosya - %s
|
||||
macWarnings.message = Cryptomator bu satırlarda kötücül olma olasılığına sahip bozulmalar tespit etti\:
|
||||
macWarnings.moreInformationButton = Daha fazla öğren
|
||||
macWarnings.whitelistButton = Seçilenleri her halükarda şifrele
|
||||
# settings.fxml
|
||||
settings.version.label = Sürüm %s
|
||||
settings.checkForUpdates.label = Güncellemeleri denetle
|
||||
settings.port.label = WebDAV Port *
|
||||
settings.port.prompt = 0 \= Otomatik seç
|
||||
settings.useipv6.label = IPv6 kullan
|
||||
settings.requiresRestartLabel = * Cryptomator yeniden başlatılması gerek
|
||||
# tray icon
|
||||
tray.menu.open = Aç
|
||||
tray.menu.quit = Çıkış
|
||||
tray.infoMsg.title = Hala çalışıyor
|
||||
tray.infoMsg.msg = Cryptomator hala çalışıyor. Bildirim simgesi ile çıkış yapın.
|
||||
tray.infoMsg.msg.osx = Cryptomator hala çalışıyor. Menü bar simgesi ile çıkış yapın.
|
||||
initialize.messageLabel.passwordStrength.0 = Çok zayıf
|
||||
initialize.messageLabel.passwordStrength.1 = Zayıf
|
||||
initialize.messageLabel.passwordStrength.2 = İyi
|
||||
initialize.messageLabel.passwordStrength.3 = Güçlü
|
||||
initialize.messageLabel.passwordStrength.4 = Çok güçlü
|
||||
Reference in New Issue
Block a user