package com.subgraph.orchid.directory; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; import java.util.List; import java.util.logging.Logger; import com.subgraph.orchid.Document; import com.subgraph.orchid.TorConfig; import com.subgraph.orchid.crypto.TorRandom; public class DirectoryStoreFile { private final static Logger logger = Logger.getLogger(DirectoryStoreFile.class.getName()); private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); private final static TorRandom random = new TorRandom(); private final TorConfig config; private final String cacheFilename; private RandomAccessFile openFile; private boolean openFileFailed; private boolean directoryCreationFailed; DirectoryStoreFile(TorConfig config, String cacheFilename) { this.config = config; this.cacheFilename = cacheFilename; } public void writeData(ByteBuffer data) { final File tempFile = createTempFile(); final FileOutputStream fos = openFileOutputStream(tempFile); if(fos == null) { return; } try { writeAllToChannel(fos.getChannel(), data); quietClose(fos); installTempFile(tempFile); } catch (IOException e) { logger.warning("I/O error writing to temporary cache file "+ tempFile + " : "+ e); return; } finally { quietClose(fos); tempFile.delete(); } } public void writeDocuments(List<? extends Document> documents) { final File tempFile = createTempFile(); final FileOutputStream fos = openFileOutputStream(tempFile); if(fos == null) { return; } try { writeDocumentsToChannel(fos.getChannel(), documents); quietClose(fos); installTempFile(tempFile); } catch (IOException e) { logger.warning("I/O error writing to temporary cache file "+ tempFile + " : "+ e); return; } finally { quietClose(fos); tempFile.delete(); } } private FileOutputStream openFileOutputStream(File file) { try { createDirectoryIfMissing(); return new FileOutputStream(file); } catch (FileNotFoundException e) { logger.warning("Failed to open file "+ file + " : "+ e); return null; } } public void appendDocuments(List<? extends Document> documents) { if(!ensureOpened()) { return; } try { final FileChannel channel = openFile.getChannel(); channel.position(channel.size()); writeDocumentsToChannel(channel, documents); channel.force(true); } catch (IOException e) { logger.warning("I/O error writing to cache file "+ cacheFilename); return; } } public ByteBuffer loadContents() { if(!(fileExists() && ensureOpened())) { return EMPTY_BUFFER; } try { return readAllFromChannel(openFile.getChannel()); } catch (IOException e) { logger.warning("I/O error reading cache file "+ cacheFilename + " : "+ e); return EMPTY_BUFFER; } } private ByteBuffer readAllFromChannel(FileChannel channel) throws IOException { channel.position(0); final ByteBuffer buffer = createBufferForChannel(channel); while(buffer.hasRemaining()) { if(channel.read(buffer) == -1) { logger.warning("Unexpected EOF reading from cache file"); return EMPTY_BUFFER; } } buffer.rewind(); return buffer; } private ByteBuffer createBufferForChannel(FileChannel channel) throws IOException { final int sz = (int) (channel.size() & 0xFFFFFFFF); return ByteBuffer.allocateDirect(sz); } void close() { if(openFile != null) { quietClose(openFile); openFile = null; } } private boolean fileExists() { final File file = getFile(); return file.exists(); } private boolean ensureOpened() { if(openFileFailed) { return false; } if(openFile != null) { return true; } openFile = openFile(); return openFile != null; } private RandomAccessFile openFile() { try { final File f = new File(config.getDataDirectory(), cacheFilename); createDirectoryIfMissing(); return new RandomAccessFile(f, "rw"); } catch (FileNotFoundException e) { openFileFailed = true; logger.warning("Failed to open cache file "+ cacheFilename); return null; } } private void installTempFile(File tempFile) { close(); final File target = getFile(); if(target.exists() && !target.delete()) { logger.warning("Failed to delete file "+ target); } if(!tempFile.renameTo(target)) { logger.warning("Failed to rename temp file "+ tempFile +" to "+ target); } tempFile.delete(); ensureOpened(); } private File createTempFile() { final long n = random.nextLong(); final File f = new File(config.getDataDirectory(), cacheFilename + Long.toString(n)); f.deleteOnExit(); return f; } private void writeDocumentsToChannel(FileChannel channel, List<? extends Document> documents) throws IOException { for(Document d: documents) { writeAllToChannel(channel, d.getRawDocumentBytes()); } } private void writeAllToChannel(WritableByteChannel channel, ByteBuffer data) throws IOException { data.rewind(); while(data.hasRemaining()) { channel.write(data); } } private void quietClose(Closeable closeable) { try { closeable.close(); } catch (IOException e) {} } private File getFile() { return new File(config.getDataDirectory(), cacheFilename); } public void remove() { close(); getFile().delete(); } private void createDirectoryIfMissing() { if(directoryCreationFailed) { return; } final File dd = config.getDataDirectory(); if(!dd.exists()) { if(!dd.mkdirs()) { directoryCreationFailed = true; logger.warning("Failed to create data directory "+ dd); } } } }