DefaultFileManager.java

  1. package net.morimekta.providence.storage.dir;

  2. import net.morimekta.util.FileUtil;

  3. import javax.annotation.Nonnull;
  4. import java.io.IOException;
  5. import java.io.UncheckedIOException;
  6. import java.nio.file.FileVisitResult;
  7. import java.nio.file.FileVisitor;
  8. import java.nio.file.Files;
  9. import java.nio.file.Path;
  10. import java.nio.file.attribute.BasicFileAttributes;
  11. import java.util.Collection;
  12. import java.util.HashSet;
  13. import java.util.function.Function;

  14. /**
  15.  * File manager for the {@link net.morimekta.providence.storage.DirectoryMessageStore}
  16.  * and {@link net.morimekta.providence.storage.DirectoryMessageListStore} store
  17.  * classes that keeps all files in a plain directory tree, and keeps a <code>.tmp</code>
  18.  * directory for temporary files.
  19.  *
  20.  * @param <K> The key type.
  21.  */
  22. public class DefaultFileManager<K> implements FileManager<K> {
  23.     private static final String TMP_DIR = ".tmp";

  24.     private final Path                directory;
  25.     private final Path                tempDir;
  26.     private final Function<Path, K> keyParser;
  27.     private final Function<K, Path> keyBuilder;

  28.     public DefaultFileManager(@Nonnull Path directory,
  29.                               @Nonnull Function<K, Path> keyBuilder,
  30.                               @Nonnull Function<Path, K> keyParser) {
  31.         try {
  32.             if (!Files.isDirectory(directory)) {
  33.                 throw new IllegalArgumentException("Not a directory: " + directory.toString());
  34.             }
  35.             this.directory = FileUtil.readCanonicalPath(directory);
  36.             this.tempDir = this.directory.resolve(TMP_DIR);
  37.             if (!Files.exists(tempDir)) {
  38.                 Files.createDirectories(tempDir);
  39.             } else if (!Files.isDirectory(tempDir)) {
  40.                 throw new IllegalStateException("File blocking temp directory: " + tempDir.toString());
  41.             }
  42.             this.keyBuilder = keyBuilder;
  43.             this.keyParser = keyParser;
  44.         } catch (IOException e) {
  45.             throw new UncheckedIOException(e.getMessage(), e);
  46.         }
  47.     }

  48.     @Override
  49.     public Path getFileFor(@Nonnull K key) {
  50.         Path path = keyBuilder.apply(key);
  51.         return directory.resolve(validatePath(path, path));
  52.     }

  53.     @Override
  54.     public Path tmpFileFor(@Nonnull K key) {
  55.         Path path = keyBuilder.apply(key);
  56.         return tempDir.resolve(validatePath(path, path));
  57.     }

  58.     @Override
  59.     public Collection<K> initialKeySet() {
  60.         try {
  61.             HashSet<K> set = new HashSet<>();
  62.             Files.walkFileTree(directory, new PathFileVisitor(set));
  63.             return set;
  64.         } catch (IOException e) {
  65.             throw new IllegalStateException("Storage directory no longer a directory.", e);
  66.         }
  67.     }

  68.     private Path validatePath(Path key, Path originalKey) {
  69.         if (key.isAbsolute()) {
  70.             throw new IllegalArgumentException("Absolute path key not allowed: " + originalKey);
  71.         }
  72.         if (key.getFileName().toString().isEmpty()) {
  73.             throw new IllegalArgumentException("Empty segment in path key: " + originalKey);
  74.         }
  75.         // Make sure that no '.' or '..' part of the path. Meaning each part of the
  76.         // path must be non-hidden entity.
  77.         if (key.getFileName().toString().startsWith(".")) {
  78.             throw new IllegalArgumentException("Special '.' not allowed in path segment start: " + originalKey);
  79.         }
  80.         if (key.getParent() != null) {
  81.             validatePath(key.getParent(), originalKey);
  82.         }
  83.         return key;
  84.     }

  85.     private class PathFileVisitor implements FileVisitor<Path> {
  86.         private final HashSet<K> set;

  87.         public PathFileVisitor(HashSet<K> set) {this.set = set;}

  88.         @Override
  89.         public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes) {
  90.             if (basicFileAttributes.isSymbolicLink() || path.getFileName().startsWith(".")) {
  91.                 return FileVisitResult.SKIP_SUBTREE;
  92.             }
  93.             return FileVisitResult.CONTINUE;
  94.         }

  95.         @Override
  96.         public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
  97.             if (basicFileAttributes.isSymbolicLink()) {
  98.                 return FileVisitResult.CONTINUE;
  99.             }
  100.             set.add(keyParser.apply(directory.relativize(path)));
  101.             return FileVisitResult.CONTINUE;
  102.         }

  103.         @Override
  104.         public FileVisitResult visitFileFailed(Path path, IOException e) {
  105.             return FileVisitResult.TERMINATE;
  106.         }

  107.         @Override
  108.         public FileVisitResult postVisitDirectory(Path path, IOException e) {
  109.             return FileVisitResult.CONTINUE;
  110.         }
  111.     }
  112. }