Transparent conflict detection for long file names

This commit is contained in:
Sebastian Stenzel 2016-05-22 15:16:32 +02:00
parent 3dcebb1e1f
commit bf05c59c3b
4 changed files with 137 additions and 2 deletions

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}