DefaultFileManager.java
package net.morimekta.providence.storage.dir;
import net.morimekta.util.FileUtil;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.HashSet;
import java.util.function.Function;
/**
* File manager for the {@link net.morimekta.providence.storage.DirectoryMessageStore}
* and {@link net.morimekta.providence.storage.DirectoryMessageListStore} store
* classes that keeps all files in a plain directory tree, and keeps a <code>.tmp</code>
* directory for temporary files.
*
* @param <K> The key type.
*/
public class DefaultFileManager<K> implements FileManager<K> {
private static final String TMP_DIR = ".tmp";
private final Path directory;
private final Path tempDir;
private final Function<Path, K> keyParser;
private final Function<K, Path> keyBuilder;
public DefaultFileManager(@Nonnull Path directory,
@Nonnull Function<K, Path> keyBuilder,
@Nonnull Function<Path, K> keyParser) {
try {
if (!Files.isDirectory(directory)) {
throw new IllegalArgumentException("Not a directory: " + directory.toString());
}
this.directory = FileUtil.readCanonicalPath(directory);
this.tempDir = this.directory.resolve(TMP_DIR);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
} else if (!Files.isDirectory(tempDir)) {
throw new IllegalStateException("File blocking temp directory: " + tempDir.toString());
}
this.keyBuilder = keyBuilder;
this.keyParser = keyParser;
} catch (IOException e) {
throw new UncheckedIOException(e.getMessage(), e);
}
}
@Override
public Path getFileFor(@Nonnull K key) {
Path path = keyBuilder.apply(key);
return directory.resolve(validatePath(path, path));
}
@Override
public Path tmpFileFor(@Nonnull K key) {
Path path = keyBuilder.apply(key);
return tempDir.resolve(validatePath(path, path));
}
@Override
public Collection<K> initialKeySet() {
try {
HashSet<K> set = new HashSet<>();
Files.walkFileTree(directory, new PathFileVisitor(set));
return set;
} catch (IOException e) {
throw new IllegalStateException("Storage directory no longer a directory.", e);
}
}
private Path validatePath(Path key, Path originalKey) {
if (key.isAbsolute()) {
throw new IllegalArgumentException("Absolute path key not allowed: " + originalKey);
}
if (key.getFileName().toString().isEmpty()) {
throw new IllegalArgumentException("Empty segment in path key: " + originalKey);
}
// Make sure that no '.' or '..' part of the path. Meaning each part of the
// path must be non-hidden entity.
if (key.getFileName().toString().startsWith(".")) {
throw new IllegalArgumentException("Special '.' not allowed in path segment start: " + originalKey);
}
if (key.getParent() != null) {
validatePath(key.getParent(), originalKey);
}
return key;
}
private class PathFileVisitor implements FileVisitor<Path> {
private final HashSet<K> set;
public PathFileVisitor(HashSet<K> set) {this.set = set;}
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) {
if (basicFileAttributes.isSymbolicLink() || path.getFileName().startsWith(".")) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
if (basicFileAttributes.isSymbolicLink()) {
return FileVisitResult.CONTINUE;
}
set.add(keyParser.apply(directory.relativize(path)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path path, IOException e) {
return FileVisitResult.TERMINATE;
}
@Override
public FileVisitResult postVisitDirectory(Path path, IOException e) {
return FileVisitResult.CONTINUE;
}
}
}