package io.fathom.cloud.blobs; import java.io.File; import java.io.IOException; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fathomdb.io.IoUtils; import com.fathomdb.utils.Hex; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.ByteSource; import com.google.common.io.Files; import com.google.protobuf.ByteString; public class LocalFilesystemBlobStore extends BlobStoreBase { private static final Logger log = LoggerFactory.getLogger(LocalFilesystemBlobStore.class); public static class Factory implements BlobStoreFactory { final String baseDir; public Factory(String baseDir) { this.baseDir = baseDir; } @Override public BlobStore get(String key) throws IOException { Preconditions.checkArgument(!Strings.isNullOrEmpty(key)); Preconditions.checkArgument(isSafeFileName(key)); File dir = new File(baseDir, key); return new LocalFilesystemBlobStore(dir); } } final File basedir; final File tmpdir; final File queueDir; public LocalFilesystemBlobStore(File basedir) throws IOException { super(); this.basedir = basedir; this.tmpdir = new File(basedir, "_tmp"); IoUtils.mkdirs(this.tmpdir); this.queueDir = new File(basedir, "_queue"); } public static boolean isSafeFileName(String key) { int len = key.length(); for (int i = 0; i < len; i++) { char c = key.charAt(i); if (Character.isLetterOrDigit(c)) { continue; } switch (c) { case '_': case '-': break; default: return false; } } return true; } @Override public BlobData find(ByteString key) throws IOException { File file = buildFile(key); if (file.exists()) { return new BlobData(file, key); } else { return null; } } @Override public boolean has(ByteString key, boolean checkCache) { File file = buildFile(key); return file.exists(); } private File buildFile(ByteString key, boolean mkdirs) throws IOException { File file = buildFile(key); if (mkdirs) { // TODO: Cache created / not created state? IoUtils.mkdirs(file.getParentFile()); } return file; } private File buildFile(ByteString key) { File file = buildFile(basedir, key); return file; } @Override public void put(BlobData data) throws IOException { ByteString key = data.getHash(); File file = buildFile(key, true); if (file.exists()) { // TODO: Verify that the file contents match?? if (isSame(file, data)) { return; } else { throw new UnsupportedOperationException(); } } try (TempFile tempFile = TempFile.in(tmpdir)) { tempFile.copyFrom(data); tempFile.renameTo(file); } } private boolean isSame(File a, BlobData b) throws IOException { ByteSource supplierA = Files.asByteSource(a); // ByteSource supplierB = ByteStreams.asByteSource(b); return supplierA.contentEquals(b); } @Override public Iterable<ByteString> listWithPrefix(String prefix) throws IOException { List<ByteString> ret = Lists.newArrayList(); for (File dir : basedir.listFiles()) { if (!dir.isDirectory()) { continue; } String name = dir.getName(); if (name.charAt(0) == '_') { continue; } if (prefix.length() > name.length()) { if (!prefix.startsWith(name)) { continue; } } else { if (!name.startsWith(prefix)) { continue; } } listWithPrefix(ret, dir, prefix); } return ret; } private void listWithPrefix(List<ByteString> ret, File dir, String prefix) { for (File file : dir.listFiles()) { if (!file.isFile()) { continue; } String name = file.getName(); if (!name.startsWith(prefix)) { continue; } try { byte[] bytes = Hex.fromHex(name); ret.add(ByteString.copyFrom(bytes)); } catch (IllegalArgumentException e) { log.warn("Unable to parse filename: " + name, e); continue; } } } }