/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.support.io; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.Random; import freenet.crypt.EncryptedRandomAccessBucket; import freenet.crypt.EncryptedRandomAccessBufferType; import freenet.crypt.MasterSecret; import freenet.crypt.RandomSource; import freenet.keys.CHKBlock; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.api.BucketFactory; import freenet.support.api.RandomAccessBucket; /** * Handles persistent temp files. These are used for e.g. persistent downloads. These are * temporary files in the directory specified for the PersistentFileTracker (which supports * changing the directory, i.e. moving the files). * * These temporary files are encrypted using an ephemeral key (unless the node is configured not to encrypt * temporary files as happens with physical security level LOW). FIXME NO CRYPTO AT THE MOMENT. * * Note that the files are only deleted *after* the transaction containing their deletion reaches * disk - so we should not leak temporary files, or forget that we deleted a bucket and try to * reuse it, if there is an unclean shutdown. * * PERSISTENCE: This class is involved in persistence but is not itself Serializable; it is * recreated on every startup, and persistent Bucket's register themselves with it. */ public class PersistentTempBucketFactory implements BucketFactory, PersistentFileTracker { /** Original contents of directory. This used to be used to delete any files that we can't account for. * However at the moment we do not support garbage collection for non-blob persistent temp files. * When we implement it it will probably not use this structure... FIXME! */ private HashSet<File> originalFiles; /** Filename generator. Tracks the directory and the prefix for temp files, can move them if these * change, generates filenames. */ public final FilenameGenerator fg; /** Cryptographically strong random number generator */ private transient RandomSource strongPRNG; /** Weak but fast random number generator. */ private transient Random weakPRNG; /** Buckets to free. When buckets are freed, we write them to this list, and delete the files *after* * the transaction recording the buckets being deleted hits the disk. */ private final ArrayList<DelayedFree> bucketsToFree; private final Object encryptLock = new Object(); /** Should we encrypt temporary files? */ private boolean encrypt; private MasterSecret secret; private DiskSpaceChecker checker; private long commitID; static final int BLOB_SIZE = CHKBlock.DATA_LENGTH; private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } /** * Create a temporary bucket factory. * @param dir Where to put it. * @param prefix Prefix for temporary file names. * @param strongPRNG Cryptographically strong random number generator, for making keys etc. * @param weakPRNG Weak but fast random number generator. * @param encrypt Whether to encrypt temporary files. * @throws IOException If we are unable to read the directory, etc. */ public PersistentTempBucketFactory(File dir, final String prefix, RandomSource strongPRNG, Random weakPRNG, boolean encrypt) throws IOException { this.strongPRNG = strongPRNG; this.weakPRNG = weakPRNG; this.encrypt = encrypt; this.fg = new FilenameGenerator(weakPRNG, false, dir, prefix); if(!dir.exists()) { dir.mkdir(); if(!dir.exists()) { throw new IOException("Directory does not exist and cannot be created: "+dir); } } if(!dir.isDirectory()) throw new IOException("Directory is not a directory: "+dir); originalFiles = new HashSet<File>(); File[] files = dir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { if(!pathname.exists() || pathname.isDirectory()) return false; String name = pathname.getName(); if(name.startsWith(prefix)) return true; return false; } }); for(File f : files) { f = FileUtil.getCanonicalFile(f); if(logMINOR) Logger.minor(this, "Found " + f); originalFiles.add(f); } bucketsToFree = new ArrayList<DelayedFree>(); commitID = 1; // Must start > 0. } public void setDiskSpaceChecker(DiskSpaceChecker checker) { this.checker = checker; } public void setMasterSecret(MasterSecret secret) { synchronized(encryptLock) { this.secret = secret; } } /** Notify the bucket factory that a file is a temporary file, and not to be deleted. FIXME this is not * currently used. @see #completedInit() */ @Override public void register(File file) { synchronized(this) { if(originalFiles == null) throw new IllegalStateException("completed Init has already been called!"); file = FileUtil.getCanonicalFile(file); if(logMINOR) Logger.minor(this, "Preserving "+file, new Exception("debug")); if(!originalFiles.remove(file)) Logger.error(this, "Preserving "+file+" but it wasn't found!", new Exception("error")); } } /** * Called when boot-up is complete. * Deletes any old temp files still unclaimed. */ public synchronized void completedInit() { if(originalFiles == null) { Logger.error(this, "Completed init called twice", new Exception("error")); return; } for(File f: originalFiles) { if(Logger.shouldLog(LogLevel.MINOR, this)) Logger.minor(this, "Deleting old tempfile "+f); f.delete(); } originalFiles = null; } /** Create a persistent temporary bucket. Encrypted if appropriate. Wrapped in a * DelayedFreeBucket so that they will not be deleted until after the transaction deleting * them in the database commits. */ @Override public RandomAccessBucket makeBucket(long size) throws IOException { RandomAccessBucket rawBucket = null; boolean mustWrap = true; if(rawBucket == null) rawBucket = new PersistentTempFileBucket(fg.makeRandomFilename(), fg, this); synchronized(encryptLock) { if(encrypt) { rawBucket = new PaddedRandomAccessBucket(rawBucket); rawBucket = new EncryptedRandomAccessBucket(TempBucketFactory.CRYPT_TYPE, rawBucket, secret); } } if(mustWrap) rawBucket = new DelayedFreeRandomAccessBucket(this, rawBucket); return rawBucket; } /** * Free an allocated bucket, but only after the change has been written to disk. */ @Override public void delayedFree(DelayedFree b, long createdCommitID) { synchronized(this) { if(createdCommitID != commitID) { bucketsToFree.add(b); return; } } b.realFree(); } /** Returns a list of buckets to free. The caller should write the buckets to the checkpoint, * and free them after the checkpoint has written successfully, by calling postCommit(). */ public DelayedFree[] grabBucketsToFree() { synchronized(this) { if(bucketsToFree.isEmpty()) return null; DelayedFree[] buckets = bucketsToFree.toArray(new DelayedFree[bucketsToFree.size()]); bucketsToFree.clear(); commitID++; return buckets; } } @Override public synchronized long commitID() { return commitID; } /** Get the directory we are creating temporary files in */ @Override public File getDir() { return fg.getDir(); } /** Get the FilenameGenerator */ @Override public FilenameGenerator getGenerator() { return fg; } /** Are we encrypting temporary files? */ public boolean isEncrypting() { synchronized(encryptLock) { return encrypt; } } /** * Set whether to encrypt new persistent temp buckets. Note that we do not encrypt/decrypt old ones when * this changes. */ public void setEncryption(boolean encrypt) { synchronized(encryptLock) { this.encrypt = encrypt; } } /** * Delete the buckets. */ public void finishDelayedFree(DelayedFree[] buckets) { if(buckets != null) { for(DelayedFree bucket : buckets) { try { if(bucket.toFree()) bucket.realFree(); } catch (Throwable t) { Logger.error(this, "Caught "+t+" freeing bucket "+bucket+" after transaction commit", t); } } } } @Override public boolean checkDiskSpace(File file, int toWrite, int bufferSize) { return checker.checkDiskSpace(file, toWrite, bufferSize); } }