diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java index 10f8a4638..7276d9b15 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java @@ -93,7 +93,7 @@ public class ConflictResolverTest { } @Test - public void testConflictingFileIfCanonicalDoesnExist() { + public void testConflictingFileIfCanonicalDoesntExist() { Mockito.when(canonicalFile.exists()).thenReturn(false); File resolved = conflictResolver.resolveIfNecessary(conflictingFile); Mockito.verify(conflictingFile).moveTo(canonicalFile); diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java new file mode 100644 index 000000000..ac8b056fe --- /dev/null +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ConflictResolver.java @@ -0,0 +1,70 @@ +package org.cryptomator.filesystem.shortening; + +import java.util.UUID; +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 String LONG_NAME_FILE_EXT = ".lng"; + private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}"); + private static final int UUID_FIRST_GROUP_STRLEN = 8; + + private ConflictResolver() { + } + + public static File resolveConflictIfNecessary(File potentiallyConflictingFile, FilenameShortener shortener) { + String shortName = potentiallyConflictingFile.name(); + String basename = StringUtils.removeEnd(shortName, LONG_NAME_FILE_EXT); + Matcher matcher = BASE32_PATTERN.matcher(basename); + if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.matches()) { + // no conflict. + return potentiallyConflictingFile; + } else if (shortName.endsWith(LONG_NAME_FILE_EXT) && matcher.find(0)) { + String canonicalShortName = matcher.group() + LONG_NAME_FILE_EXT; + return resolveConflict(potentiallyConflictingFile, canonicalShortName, shortener); + } else { + // not even shortened at all. + return potentiallyConflictingFile; + } + } + + private static File resolveConflict(File conflictingFile, String canonicalShortName, FilenameShortener shortener) { + Folder parent = conflictingFile.parent().get(); + File canonicalFile = parent.file(canonicalShortName); + if (canonicalFile.exists()) { + // foo (1).lng -> bar.lng + String canonicalLongName = shortener.inflate(canonicalShortName); + String alternativeLongName; + String alternativeShortName; + File alternativeFile; + String conflictId; + do { + conflictId = createConflictId(); + alternativeLongName = canonicalLongName + " (Conflict " + conflictId + ")"; + alternativeShortName = shortener.deflate(alternativeLongName); + alternativeFile = parent.file(alternativeShortName); + } while (alternativeFile.exists()); + LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile); + conflictingFile.moveTo(alternativeFile); + shortener.saveMapping(alternativeLongName, alternativeShortName); + return alternativeFile; + } else { + // foo (1).lng -> foo.lng + conflictingFile.moveTo(canonicalFile); + return canonicalFile; + } + } + + private static String createConflictId() { + return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN); + } + +} diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFile.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFile.java index 22fa0145d..919a2a8a4 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFile.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFile.java @@ -22,7 +22,7 @@ class ShorteningFile extends DelegatingFile { private final FilenameShortener shortener; public ShorteningFile(ShorteningFolder parent, File delegate, String name, FilenameShortener shortener) { - super(parent, delegate); + super(parent, ConflictResolver.resolveConflictIfNecessary(delegate, shortener)); this.longName = new AtomicReference<>(name); this.shortener = shortener; } diff --git a/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ConflictResolverTest.java b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ConflictResolverTest.java new file mode 100644 index 000000000..51d7c2015 --- /dev/null +++ b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ConflictResolverTest.java @@ -0,0 +1,65 @@ +package org.cryptomator.filesystem.shortening; + +import java.util.Optional; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.cryptomator.filesystem.inmem.InMemoryFileSystem; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class ConflictResolverTest { + + private Folder metadataFolder; + private FilenameShortener shortener; + private Folder folder; + private File canonicalFile; + private File conflictingFile; + private File resolvedFile; + + @Before + public void setup() { + metadataFolder = new InMemoryFileSystem(); + shortener = new FilenameShortener(metadataFolder, 20); + folder = Mockito.mock(Folder.class); + canonicalFile = Mockito.mock(File.class); + conflictingFile = Mockito.mock(File.class); + resolvedFile = Mockito.mock(File.class); + + String longName = "hello world, I am a very long file name. certainly longer than twenty characters.exe"; + String shortName = shortener.deflate(longName); + shortener.saveMapping(longName, shortName); + String canonicalFileName = shortName; + String conflictingFileName = shortName.replace(".lng", " (1).lng"); + + Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName); + Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName); + + Mockito.when(canonicalFile.exists()).thenReturn(true); + Mockito.when(conflictingFile.exists()).thenReturn(true); + + Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent(); + Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent(); + + Mockito.when(folder.file(Mockito.anyString())).thenReturn(resolvedFile); + Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile); + Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile); + } + + @Test + public void testNoConflictToBeResolved() { + File resolved = ConflictResolver.resolveConflictIfNecessary(canonicalFile, new FilenameShortener(metadataFolder, 20)); + Mockito.verify(conflictingFile, Mockito.never()).moveTo(Mockito.any()); + Assert.assertSame(canonicalFile, resolved); + } + + @Test + public void testConflictToBeResolved() { + File resolved = ConflictResolver.resolveConflictIfNecessary(conflictingFile, new FilenameShortener(metadataFolder, 20)); + Mockito.verify(conflictingFile).moveTo(resolvedFile); + Assert.assertSame(resolvedFile, resolved); + } + +}