/* 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);
}
}