Browse Source

Transparent conflict detection for long file names

Sebastian Stenzel 9 years ago
parent
commit
bf05c59c3b

+ 1 - 1
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);

+ 70 - 0
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);
+	}
+
+}

+ 1 - 1
main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFile.java

@@ -22,7 +22,7 @@ class ShorteningFile extends DelegatingFile<ShorteningFolder> {
 	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;
 	}

+ 65 - 0
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);
+	}
+
+}