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