From 47712a39aca0606f34eca9762cf12c5624a43885 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 27 Jan 2014 13:41:38 +0000
Subject: [PATCH] i2psnark:  - Support arbitrary location for torrent data.
 Save location in    per-torrent config file. TODO: Fix torrent browse pages  
  (ticket #1028)  - Enhance idle shutdown message  - Javadocs

---
 .../java/src/org/klomp/snark/IdleChecker.java |  3 +-
 .../java/src/org/klomp/snark/PeerState.java   |  2 +-
 .../java/src/org/klomp/snark/Snark.java       | 49 ++++++++++++--
 .../src/org/klomp/snark/SnarkManager.java     | 64 ++++++++++++++-----
 .../java/src/org/klomp/snark/Storage.java     | 21 +++---
 .../org/klomp/snark/web/I2PSnarkServlet.java  | 46 +++++++++----
 6 files changed, 138 insertions(+), 47 deletions(-)

diff --git a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java
index ff6454ee23..58a73b668f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/IdleChecker.java
@@ -66,7 +66,8 @@ class IdleChecker extends SimpleTimer2.TimedEvent {
                     if (_log.shouldLog(Log.WARN))
                         _log.warn("Closing tunnels on idle");
                     _util.disconnect();
-                    _mgr.addMessage(_util.getString("I2P tunnel closed."));
+                    _mgr.addMessage(_util.getString("No more torrents running.") + ' ' +
+                                    _util.getString("I2P tunnel closed."));
                     schedule(3 * CHECK_TIME);
                     return;
                 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
index 35008394e8..cf295a530d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
@@ -163,7 +163,7 @@ class PeerState implements DataLoader
           _log.debug(peer + " rcv bitfield");
         if (bitfield != null)
           {
-            // XXX - Be liberal in what you except?
+            // XXX - Be liberal in what you accept?
             if (_log.shouldLog(Log.WARN))
               _log.warn("Got unexpected bitfield message from " + peer);
             return;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index 398d9bd6ea..20d8f4294d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -34,6 +34,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.client.streaming.I2PServerSocket;
 import net.i2p.data.Destination;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFile;
 
 /**
  * Main Snark program startup class.
@@ -238,13 +239,21 @@ public class Snark
   private volatile boolean _autoStoppable;
 
 
-  /** from main() via parseArguments() single torrent */
+  /**
+   * from main() via parseArguments() single torrent
+   *
+   * @deprecated unused
+   */
   Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
         StorageListener slistener, CoordinatorListener clistener) { 
     this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); 
   }
 
-  /** single torrent - via router */
+  /**
+   * single torrent - via router
+   *
+   * @deprecated unused
+   */
   public Snark(I2PAppContext ctx, Properties opts, String torrent,
                StorageListener slistener, boolean start, String rootDir) { 
     this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
@@ -275,11 +284,28 @@ public class Snark
         this.startTorrent();
   }
 
-  /** multitorrent */
+  /**
+   * multitorrent
+   */
   public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
         StorageListener slistener, CoordinatorListener clistener,
         CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
         ConnectionAcceptor connectionAcceptor, boolean start, String rootDir)
+  {
+      this(util, torrent, ip, user_port, slistener, clistener, complistener,
+           peerCoordinatorSet, connectionAcceptor, start, rootDir, null);
+  }
+
+  /**
+   * multitorrent
+   *
+   * @param baseFile if null, use rootDir/torrentName; if non-null, use it instead
+   * @since 0.9.11
+   */
+  public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
+        StorageListener slistener, CoordinatorListener clistener,
+        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
+        ConnectionAcceptor connectionAcceptor, boolean start, String rootDir, File baseFile)
   {
     if (slistener == null)
       slistener = this;
@@ -395,7 +421,14 @@ public class Snark
         try
           {
             activity = "Checking storage";
-            storage = new Storage(_util, rootDataDir, meta, slistener);
+            if (baseFile == null) {
+                String base = Storage.filterName(meta.getName());
+                if (_util.getFilesPublic())
+                    baseFile = new File(rootDataDir, base);
+                else
+                    baseFile = new SecureFile(rootDataDir, base);
+            }
+            storage = new Storage(_util, baseFile, meta, slistener);
             if (completeListener != null) {
                 storage.check(completeListener.getSavedTorrentTime(this),
                               completeListener.getSavedTorrentBitField(this));
@@ -1102,8 +1135,14 @@ public class Snark
    */
   public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
       try {
+          String base = Storage.filterName(metainfo.getName());
+          File baseFile;
+          if (_util.getFilesPublic())
+              baseFile = new File(rootDataDir, base);
+          else
+              baseFile = new SecureFile(rootDataDir, base);
           // The following two may throw IOE...
-          storage = new Storage(_util, rootDataDir, metainfo, this);
+          storage = new Storage(_util, baseFile, metainfo, this);
           storage.check();
           // ... so don't set meta until here
           meta = metainfo;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index dc1a9e92d6..92a14e400a 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -86,6 +86,7 @@ public class SnarkManager implements CompleteListener {
     public static final String PROP_DIR = "i2psnark.dir";
     private static final String PROP_META_PREFIX = "i2psnark.zmeta.";
     private static final String PROP_META_STAMP = "stamp";
+    private static final String PROP_META_BASE = "base";
     private static final String PROP_META_BITFIELD = "bitfield";
     private static final String PROP_META_PRIORITY = "priority";
     private static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
@@ -347,7 +348,7 @@ public class SnarkManager implements CompleteListener {
      *
      *  @return the new config directory, non-null
      *  @throws RuntimeException on creation fail
-     *  @since 0.9.10
+     *  @since 0.9.11
      */
     private File migrateConfig(File oldFile) {
         File dir = new SecureDirectory(oldFile + CONFIG_DIR_SUFFIX);
@@ -455,7 +456,7 @@ public class SnarkManager implements CompleteListener {
     /**
      *  The config for a torrent
      *  @return non-null, possibly empty
-     *  @since 0.9.10
+     *  @since 0.9.11
      */
     private Properties getConfig(Snark snark) {
         return getConfig(snark.getInfoHash());
@@ -465,7 +466,7 @@ public class SnarkManager implements CompleteListener {
      *  The config for a torrent
      *  @param ih 20-byte infohash
      *  @return non-null, possibly empty
-     *  @since 0.9.10
+     *  @since 0.9.11
      */
     private Properties getConfig(byte[] ih) {
         Properties rv = new OrderedProperties();
@@ -482,7 +483,7 @@ public class SnarkManager implements CompleteListener {
      *  The config file for a torrent
      *  @param confDir the config directory
      *  @param ih 20-byte infohash
-     *  @since 0.9.10
+     *  @since 0.9.11
      */
     private static File configFile(File confDir, byte[] ih) {
         String hex = I2PSnarkUtil.toHex(ih);
@@ -1071,15 +1072,23 @@ public class SnarkManager implements CompleteListener {
 
     /**
      *  Caller must verify this torrent is not already added.
+     *
+     *  @param filename the absolute path to save the metainfo to, generally ending in ".torrent"
+     *  @param baseFile may be null, if so look in rootDataDir
      *  @throws RuntimeException via Snark.fatal()
      */
-    private void addTorrent(String filename) { addTorrent(filename, false); }
+    private void addTorrent(String filename) {
+        addTorrent(filename, null, false);
+    }
 
     /**
      *  Caller must verify this torrent is not already added.
+     *
+     *  @param filename the absolute path to save the metainfo to, generally ending in ".torrent"
+     *  @param baseFile may be null, if so look in rootDataDir
      *  @throws RuntimeException via Snark.fatal()
      */
-    private void addTorrent(String filename, boolean dontAutoStart) {
+    private void addTorrent(String filename, File baseFile, boolean dontAutoStart) {
         if ((!dontAutoStart) && !_util.connected()) {
             addMessage(_("Connecting to I2P"));
             boolean ok = _util.connect();
@@ -1160,9 +1169,13 @@ public class SnarkManager implements CompleteListener {
                     } else {
                         // TODO load saved closest DHT nodes and pass to the Snark ?
                         // This may take a LONG time
+                        if (baseFile == null)
+                            baseFile = getSavedBaseFile(info.getInfoHash());
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info("New Snark, torrent: " + filename + " base: " + baseFile);
                         torrent = new Snark(_util, filename, null, -1, null, null, this,
                                             _peerCoordinatorSet, _connectionAcceptor,
-                                            false, dataDir.getPath());
+                                            false, dataDir.getPath(), baseFile);
                         loadSavedFilePriorities(torrent);
                         synchronized (_snarks) {
                             _snarks.put(filename, torrent);
@@ -1305,14 +1318,17 @@ public class SnarkManager implements CompleteListener {
      * This verifies that a torrent with this infohash is not already added.
      * This may take a LONG time to create or check the storage.
      *
+     * Called from servlet.
+     *
      * @param metainfo the metainfo for the torrent
      * @param bitfield the current completion status of the torrent
      * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent
      *                 Must be a filesystem-safe name.
+     * @param baseFile may be null, if so look in rootDataDir
      * @throws RuntimeException via Snark.fatal()
      * @since 0.8.4
      */
-    public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException {
+    public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, File baseFile, boolean dontAutoStart) throws IOException {
         // prevent interference by DirMonitor
         synchronized (_snarks) {
             Snark snark = getTorrentByInfoHash(metainfo.getInfoHash());
@@ -1321,11 +1337,11 @@ public class SnarkManager implements CompleteListener {
                 return;
             }
             // so addTorrent won't recheck
-            saveTorrentStatus(metainfo, bitfield, null); // no file priorities
+            saveTorrentStatus(metainfo, bitfield, null, baseFile); // no file priorities
             try {
                 locked_writeMetaInfo(metainfo, filename, areFilesPublic());
                 // hold the lock for a long time
-                addTorrent(filename, dontAutoStart);
+                addTorrent(filename, baseFile, dontAutoStart);
             } catch (IOException ioe) {
                 addMessage(_("Failed to copy torrent file to {0}", filename));
                 _log.error("Failed to write torrent file", ioe);
@@ -1461,6 +1477,19 @@ public class SnarkManager implements CompleteListener {
         }
         storage.setFilePriorities(rv);
     }
+
+    /**
+     * Get the base location for a torrent from the config file.
+     * @return File or null, doesn't necessarily exist
+     * @since 0.9.11
+     */
+    public File getSavedBaseFile(byte[] ih) {
+        Properties config = getConfig(ih);
+        String base = config.getProperty(PROP_META_BASE);
+        if (base == null)
+            return null;
+        return new File(base);
+    }
     
     /**
      * Save the completion status of a torrent and the current time in the config file
@@ -1471,14 +1500,15 @@ public class SnarkManager implements CompleteListener {
      *
      * @param bitfield non-null
      * @param priorities may be null
+     * @param base may be null
      */
-    public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
+    public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) {
         synchronized (_configLock) {
-            locked_saveTorrentStatus(metainfo, bitfield, priorities);
+            locked_saveTorrentStatus(metainfo, bitfield, priorities, base);
         }
     }
 
-    private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
+    private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) {
         byte[] ih = metainfo.getInfoHash();
         String bfs;
         if (bitfield.complete()) {
@@ -1490,6 +1520,8 @@ public class SnarkManager implements CompleteListener {
         Properties config = getConfig(ih);
         config.setProperty(PROP_META_STAMP, Long.toString(System.currentTimeMillis()));
         config.setProperty(PROP_META_BITFIELD, bfs);
+        if (base != null)
+            config.setProperty(PROP_META_BASE, base.getAbsolutePath());
 
         // now the file priorities
         if (priorities != null) {
@@ -1742,7 +1774,7 @@ public class SnarkManager implements CompleteListener {
         MetaInfo meta = snark.getMetaInfo();
         Storage storage = snark.getStorage();
         if (meta != null && storage != null)
-            saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities());
+            saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getBase());
     }
     
     /**
@@ -1764,7 +1796,7 @@ public class SnarkManager implements CompleteListener {
                 snark.stopTorrent();
                 return null;
             }
-            saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities
+            saveTorrentStatus(meta, storage.getBitField(), null, storage.getBase()); // no file priorities
             // temp for addMessage() in case canonical throws
             String name = storage.getBaseName();
             try {
@@ -1865,7 +1897,7 @@ public class SnarkManager implements CompleteListener {
                 try {
                     // Snark.fatal() throws a RuntimeException
                     // don't let one bad torrent kill the whole loop
-                    addTorrent(name, !shouldAutoStart());
+                    addTorrent(name, null, !shouldAutoStart());
                 } catch (Exception e) {
                     addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e);
                     _log.error("Unable to add the torrent " + name, e);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index e9bf050b8c..b353f4284f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -86,20 +86,18 @@ public class Storage
   private static final ByteCache _cache = ByteCache.getInstance(16, BUFSIZE);
 
   /**
-   * Creates a new storage based on the supplied MetaInfo.  This will
+   * Creates a new storage based on the supplied MetaInfo.
+   *
+   * Does not check storage. Caller MUST call check(), which will
    * try to create and/or check all needed files in the MetaInfo.
    *
-   * Does not check storage. Caller MUST call check()
+   * @param baseFile the torrent data file or dir
    */
-  public Storage(I2PSnarkUtil util, File rootDir, MetaInfo metainfo, StorageListener listener)
+  public Storage(I2PSnarkUtil util, File baseFile, MetaInfo metainfo, StorageListener listener)
   {
     _util = util;
     _log = util.getContext().logManager().getLog(Storage.class);
-    boolean areFilesPublic = _util.getFilesPublic();
-    if (areFilesPublic)
-        _base = new File(rootDir, filterName(metainfo.getName()));
-    else
-        _base = new SecureFile(rootDir, filterName(metainfo.getName()));
+    _base = baseFile;
     this.metainfo = metainfo;
     this.listener = listener;
     needed = metainfo.getPieces();
@@ -708,7 +706,8 @@ public class Storage
 
   /**
    *  The base file or directory.
-   *  @return a new List
+   *  @return the File
+   *  @since 0.9.11
    */
   public File getBase() {
       return _base;
@@ -716,8 +715,8 @@ public class Storage
 
   /**
    *  Does not include directories. Unsorted.
-   *  @since 0.9.10
    *  @return a new List
+   *  @since 0.9.11
    */
   public List<File> getFiles() {
       List<File> rv = new ArrayList<File>(_torrentFiles.size());
@@ -731,7 +730,7 @@ public class Storage
    *  Includes the base for a multi-file torrent.
    *  Sorted bottom-up for easy deletion.
    *  Slow. Use for deletion only.
-   *  @since 0.9.10
+   *  @since 0.9.11
    *  @return a new Set or null for a single-file torrent
    */
   public SortedSet<File> getDirectories() {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index be980112ab..82c0345d7c 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -896,7 +896,9 @@ public class I2PSnarkServlet extends BasicServlet {
         } else if ("Create".equals(action)) {
             String baseData = req.getParameter("baseFile");
             if (baseData != null && baseData.trim().length() > 0) {
-                File baseFile = new File(_manager.getDataDir(), baseData);
+                File baseFile = new File(baseData.trim());
+                if (!baseFile.isAbsolute())
+                    baseFile = new File(_manager.getDataDir(), baseData);
                 String announceURL = req.getParameter("announceURL");
                 // make the user add a tracker on the config form now
                 //String announceURLOther = req.getParameter("announceURLOther");
@@ -956,7 +958,7 @@ public class I2PSnarkServlet extends BasicServlet {
                         File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
                         // FIXME is the storage going to stay around thanks to the info reference?
                         // now add it, but don't automatically start it
-                        _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true);
+                        _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), baseFile, true);
                         _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
                         if (announceURL != null && !_manager.util().getOpenTrackers().contains(announceURL))
                             _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
@@ -1708,10 +1710,11 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write("</span><hr>\n<table border=\"0\"><tr><td>");
         //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n");
         out.write(_("Data to seed"));
-        out.write(":<td><code>" + _manager.getDataDir().getAbsolutePath() + File.separatorChar 
-                  + "</code><input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile 
+        out.write(":<td>"
+                  + "<input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile 
                   + "\" spellcheck=\"false\" title=\"");
-        out.write(_("File or directory to seed (must be within the specified path)"));
+        out.write(_("File or directory to seed (full path or within the directory {0} )",
+                    _manager.getDataDir().getAbsolutePath() + File.separatorChar));
         out.write("\" ><tr><td>\n");
         out.write(_("Trackers"));
         out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">");
@@ -2198,12 +2201,6 @@ public class I2PSnarkServlet extends BasicServlet {
     private String getListHTML(File r, String base, boolean parent, Map<String, String[]> postParams)
         throws IOException
     {
-        File[] ls = null;
-        if (r.isDirectory()) {
-            ls = r.listFiles();
-            Arrays.sort(ls, new ListingComparator());
-        }  // if r is not a directory, we are only showing torrent info section
-        
         String title = decodePath(base);
         String cpath = _contextPath + '/';
         if (title.startsWith(cpath))
@@ -2249,7 +2246,8 @@ public class I2PSnarkServlet extends BasicServlet {
         
         if (parent)  // always true
             buf.append("<div class=\"page\"><div class=\"mainsection\">");
-        boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete();
+        boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
+                               r.isDirectory();
         if (showPriority) {
             buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
             buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
@@ -2271,6 +2269,12 @@ public class I2PSnarkServlet extends BasicServlet {
                .append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
                .append(fullPath)
                .append("</a></td></tr>\n");
+            buf.append("<tr><td>")
+               .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" >&nbsp;<b>")
+               .append(_("Data location"))
+               .append(":</b> ")
+               .append(urlEncode(snark.getStorage().getBase().getPath()))
+               .append("</td></tr>\n");
 
             MetaInfo meta = snark.getMetaInfo();
             if (meta != null) {
@@ -2404,6 +2408,22 @@ public class I2PSnarkServlet extends BasicServlet {
                .append("\"</th></tr>\n");
         }
         buf.append("</table>\n");
+
+        if (snark != null && !r.exists()) {
+            // fixup TODO
+            buf.append("<p>Does not exist<br>resource=\"").append(r.toString())
+               .append("\"<br>base=\"").append(base)
+               .append("\"<br>torrent=\"").append(torrentName)
+               .append("\"</p></div></div></BODY></HTML>");
+            return buf.toString();
+        }
+
+        File[] ls = null;
+        if (r.isDirectory()) {
+            ls = r.listFiles();
+            Arrays.sort(ls, new ListingComparator());
+        }  // if r is not a directory, we are only showing torrent info section
+        
         if (ls == null) {
             // We are only showing the torrent info section
             buf.append("</div></div></BODY></HTML>");
@@ -2655,6 +2675,6 @@ public class I2PSnarkServlet extends BasicServlet {
             }
         }
          snark.updatePiecePriorities();
-        _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities());
+        _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities(), storage.getBase());
     }
 }
-- 
GitLab