From 6a7dbc8e3a60194fe60bb8920b6eb7c6fbc3cfff Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 18 Mar 2007 21:43:41 +0000 Subject: [PATCH] (zzz) i2psnark: Save torrent completion status in i2psnark.config --- .../java/src/org/klomp/snark/Snark.java | 2 + .../src/org/klomp/snark/SnarkManager.java | 94 ++++++++++++++++++- .../java/src/org/klomp/snark/Storage.java | 32 ++++++- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index dea8ea7586..0a907f6d30 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -468,6 +468,8 @@ public class Snark pc.halt(); Storage st = storage; if (st != null) { + if (storage.changed) + SnarkManager.instance().saveTorrentStatus(storage.getMetaInfo(), storage.getBitField()); try { storage.close(); } catch (IOException ioe) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 1897925082..f7e72eaa0f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -3,6 +3,7 @@ package org.klomp.snark; import java.io.*; import java.util.*; import net.i2p.I2PAppContext; +import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.util.I2PThread; import net.i2p.util.Log; @@ -30,6 +31,8 @@ public class SnarkManager implements Snark.CompleteListener { public static final String PROP_EEP_PORT = "i2psnark.eepPort"; public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total"; public static final String PROP_DIR = "i2psnark.dir"; + public static final String PROP_META_PREFIX = "i2psnark.zmeta."; + public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; public static final String PROP_AUTO_START = "i2snark.autoStart"; public static final String DEFAULT_AUTO_START = "false"; @@ -271,7 +274,9 @@ public class SnarkManager implements Snark.CompleteListener { public void saveConfig() { try { - DataHelper.storeProps(_config, new File(_configFile)); + synchronized (_configFile) { + DataHelper.storeProps(_config, new File(_configFile)); + } } catch (IOException ioe) { addMessage("Unable to save the config to '" + _configFile + "'"); } @@ -361,6 +366,91 @@ public class SnarkManager implements Snark.CompleteListener { } } + /** + * Get the timestamp for a torrent from the config file + */ + public long getSavedTorrentTime(MetaInfo metainfo) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (time == null) + return 0; + int comma = time.indexOf(','); + if (comma <= 0) + return 0; + time = time.substring(0, comma); + try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} + return 0; + } + + /** + * Get the saved bitfield for a torrent from the config file. + * Convert "." to a full bitfield. + */ + public BitField getSavedTorrentBitField(MetaInfo metainfo) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (bf == null) + return null; + int comma = bf.indexOf(','); + if (comma <= 0) + return null; + bf = bf.substring(comma + 1).trim(); + int len = metainfo.getPieces(); + if (bf.equals(".")) { + BitField bitfield = new BitField(len); + for (int i = 0; i < len; i++) + bitfield.set(i); + return bitfield; + } + byte[] bitfield = Base64.decode(bf); + if (bitfield == null) + return null; + if (bitfield.length * 8 < len) + return null; + return new BitField(bitfield, len); + } + + /** + * Save the completion status of a torrent and the current time in the config file + * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". + * The config file property key is appended with the Base64 of the infohash, + * with the '=' changed to '$' since a key can't contain '='. + * The time is a standard long converted to string. + * The status is either a bitfield converted to Base64 or "." for a completed + * torrent to save space in the config file and in memory. + */ + public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String now = "" + System.currentTimeMillis(); + String bfs; + if (bitfield.complete()) { + bfs = "."; + } else { + byte[] bf = bitfield.getFieldBytes(); + bfs = Base64.encode(bf); + } + _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); + saveConfig(); + } + + /** + * Remove the status of a torrent from the config file. + * This may help the config file from growing too big. + */ + public void removeTorrentStatus(MetaInfo metainfo) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + saveConfig(); + } + private String locked_validateTorrent(MetaInfo info) throws IOException { List files = info.getFiles(); if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { @@ -426,6 +516,8 @@ public class SnarkManager implements Snark.CompleteListener { if (torrent != null) { File torrentFile = new File(filename); torrentFile.delete(); + if (torrent.storage != null) + removeTorrentStatus(torrent.storage.getMetaInfo()); addMessage("Torrent removed: '" + torrentFile.getName() + "'"); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 381909807f..27b91052e5 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -45,6 +45,7 @@ public class Storage // XXX - Not always set correctly int piece_size; int pieces; + boolean changed; /** The default piece size. */ private static int MIN_PIECE_SIZE = 256*1024; @@ -285,6 +286,10 @@ public class Storage public void check(String rootDir) throws IOException { File base = new File(rootDir, filterName(metainfo.getName())); + // look for saved bitfield and timestamp in the config file + long savedTime = SnarkManager.instance().getSavedTorrentTime(metainfo); + BitField savedBitField = SnarkManager.instance().getSavedTorrentBitField(metainfo); + boolean useSavedBitField = savedTime > 0 && savedBitField != null; List files = metainfo.getFiles(); if (files == null) @@ -298,6 +303,11 @@ public class Storage rafs = new RandomAccessFile[1]; names = new String[1]; lengths[0] = metainfo.getTotalLength(); + if (useSavedBitField) { + long lm = base.lastModified(); + if (lm <= 0 || lm > savedTime) + useSavedBitField = false; + } if (base.exists() && !base.canWrite()) // hope we can get away with this, if we are only seeding... rafs[0] = new RandomAccessFile(base, "r"); else @@ -322,6 +332,11 @@ public class Storage File f = createFileFromNames(base, (List)files.get(i)); lengths[i] = ((Long)ls.get(i)).longValue(); total += lengths[i]; + if (useSavedBitField) { + long lm = base.lastModified(); + if (lm <= 0 || lm > savedTime) + useSavedBitField = false; + } if (f.exists() && !f.canWrite()) // see above re: only seeding rafs[i] = new RandomAccessFile(f, "r"); else @@ -335,7 +350,14 @@ public class Storage throw new IOException("File lengths do not add up " + total + " != " + metalength); } - checkCreateFiles(); + if (useSavedBitField) { + bitfield = savedBitField; + needed = metainfo.getPieces() - bitfield.count(); + Snark.debug("Found saved state and files unchanged, skipping check", Snark.NOTICE); + } else { + checkCreateFiles(); + SnarkManager.instance().saveTorrentStatus(metainfo, bitfield); + } } /** @@ -354,7 +376,7 @@ public class Storage if (!base.exists()) throw new IOException("Could not reopen file " + base); - if (!base.canWrite()) // hope we can get away with this, if we are only seeding... + if (complete() || !base.canWrite()) // hope we can get away with this, if we are only seeding... rafs[0] = new RandomAccessFile(base, "r"); else rafs[0] = new RandomAccessFile(base, "rw"); @@ -372,7 +394,7 @@ public class Storage File f = getFileFromNames(base, (List)files.get(i)); if (!f.exists()) throw new IOException("Could not reopen file " + f); - if (!f.canWrite()) // see above re: only seeding + if (complete() || !f.canWrite()) // see above re: only seeding rafs[i] = new RandomAccessFile(f, "r"); else rafs[i] = new RandomAccessFile(f, "rw"); @@ -517,6 +539,7 @@ public class Storage // gobble gobble } } + changed = false; } /** @@ -604,6 +627,7 @@ public class Storage } } + changed = true; if (complete) { // listener.storageCompleted(this); // do we also need to close all of the files and reopen @@ -623,6 +647,8 @@ public class Storage listener.setWantedPieces(this); Snark.debug("WARNING: Not really done, missing " + needed + " pieces", Snark.WARNING); + } else { + SnarkManager.instance().saveTorrentStatus(metainfo, bitfield); } }