From 4e558320a9424639e1e26d53dc03a9081122b876 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 10 Dec 2012 22:48:44 +0000
Subject: [PATCH]   - i2psnark: Add announce list support (BEP 12) (ticket
 #778)     Preliminary. Still todo: TrackerClient

---
 .../java/src/org/klomp/snark/MetaInfo.java    |  37 ++++-
 .../src/org/klomp/snark/SnarkManager.java     |   4 +-
 .../java/src/org/klomp/snark/Storage.java     |   6 +-
 .../org/klomp/snark/web/I2PSnarkServlet.java  | 140 ++++++++++++++----
 4 files changed, 150 insertions(+), 37 deletions(-)

diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
index a9b70041c8..810a10db08 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
@@ -61,6 +61,7 @@ public class MetaInfo
   private final byte[] piece_hashes;
   private final long length;
   private final boolean privateTorrent;
+  private final List<List<String>> announce_list;
   private Map<String, BEValue> infoMap;
 
   /**
@@ -69,9 +70,11 @@ public class MetaInfo
    *  @param announce may be null
    *  @param files null for single-file torrent
    *  @param lengths null for single-file torrent
+   *  @param announce_list may be null
    */
   MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths,
-           int piece_length, byte[] piece_hashes, long length, boolean privateTorrent)
+           int piece_length, byte[] piece_hashes, long length, boolean privateTorrent,
+           List<List<String>> announce_list)
   {
     this.announce = announce;
     this.name = name;
@@ -83,6 +86,7 @@ public class MetaInfo
     this.piece_hashes = piece_hashes;
     this.length = length;
     this.privateTorrent = privateTorrent;
+    this.announce_list = announce_list;
 
     // TODO if we add a parameter for other keys
     //if (other != null) {
@@ -141,6 +145,23 @@ public class MetaInfo
         this.announce = val.getString();
     }
 
+    // BEP 12
+    val = m.get("announce-list");
+    if (val == null) {
+        this.announce_list = null;
+    } else {
+        this.announce_list = new ArrayList();
+        List<BEValue> bl1 = val.getList();
+        for (BEValue bev : bl1) {
+            List<BEValue> bl2 = bev.getList();
+            List<String> sl2 = new ArrayList();           
+            for (BEValue bev2 : bl2) {
+                sl2.add(bev2.getString());
+            }
+            this.announce_list.add(sl2);
+        }
+    }
+
     val = m.get("info");
     if (val == null)
         throw new InvalidBEncodingException("Missing info map");
@@ -296,6 +317,15 @@ public class MetaInfo
     return announce;
   }
 
+  /**
+   * Returns a list of lists of urls.
+   *
+   * @since 0.9.5
+   */
+  public List<List<String>> getAnnounceList() {
+    return announce_list;
+  }
+
   /**
    * Returns the original 20 byte SHA1 hash over the bencoded info map.
    */
@@ -470,12 +500,13 @@ public class MetaInfo
   /**
    * Creates a copy of this MetaInfo that shares everything except the
    * announce URL.
+   * Drops any announce-list.
    */
   public MetaInfo reannounce(String announce)
   {
     return new MetaInfo(announce, name, name_utf8, files,
                         lengths, piece_length,
-                        piece_hashes, length, privateTorrent);
+                        piece_hashes, length, privateTorrent, null);
   }
 
   /**
@@ -486,6 +517,8 @@ public class MetaInfo
         Map m = new HashMap();
         if (announce != null)
             m.put("announce", announce);
+        if (announce_list != null)
+            m.put("announce-list", announce_list);
         Map info = createInfoMap();
         m.put("info", info);
         // don't save this locally, we should only do this once
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index eef6679ce2..08e2ae67bd 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -886,7 +886,9 @@ public class SnarkManager implements CompleteListener {
                         }
                     }
                 } catch (IOException ioe) {
-                    addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage());
+                    String err = _("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage();
+                    addMessage(err);
+                    _log.error(err, ioe);
                     if (sfile.exists())
                         sfile.delete();
                     return;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index a42001ba8c..036e0f39b3 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -122,6 +122,7 @@ public class Storage
    * @throws IOException when creating and/or checking files fails.
    */
   public Storage(I2PSnarkUtil util, File baseFile, String announce,
+                 List<List<String>> announce_list,
                  boolean privateTorrent, StorageListener listener)
     throws IOException
   {
@@ -182,7 +183,8 @@ public class Storage
     // TODO thread this so we can return and show something on the UI
     byte[] piece_hashes = fast_digestCreate();
     metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
-                            lengthsList, piece_size, piece_hashes, total, privateTorrent);
+                            lengthsList, piece_size, piece_hashes, total, privateTorrent,
+                            announce_list);
 
   }
 
@@ -1225,7 +1227,7 @@ public class Storage
       File file = null;
       FileOutputStream out = null;
       try {
-          Storage storage = new Storage(util, base, announce, false, null);
+          Storage storage = new Storage(util, base, announce, null, false, null);
           MetaInfo meta = storage.getMetaInfo();
           file = new File(storage.getBaseName() + ".torrent");
           out = new FileOutputStream(file);
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 ea5d751e55..2765d58731 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -61,7 +61,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     private Resource _resourceBase;
     private String _themePath;
     private String _imgPath;
-    private String _lastAnnounceURL = "";
+    private String _lastAnnounceURL;
     
     public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
  
@@ -730,18 +730,54 @@ public class I2PSnarkServlet extends DefaultServlet {
                 //if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
                 //    announceURL = announceURLOther;
 
-                if (announceURL == null || announceURL.length() <= 0)
-                    _manager.addMessage(_("Error creating torrent - you must select a tracker"));
-                else if (baseFile.exists()) {
-                    _lastAnnounceURL = announceURL;
+                if (baseFile.exists()) {
                     if (announceURL.equals("none"))
                         announceURL = null;
+                    _lastAnnounceURL = announceURL;
+                    List<String> backupURLs = new ArrayList();
+                    Enumeration e = req.getParameterNames();
+                    while (e.hasMoreElements()) {
+                         Object o = e.nextElement();
+                         if (!(o instanceof String))
+                             continue;
+                         String k = (String) o;
+                        if (k.startsWith("backup_")) {
+                            String url = k.substring(7);
+                            if (!url.equals(announceURL))
+                                backupURLs.add(url);
+                        }
+                    }
+                    List<List<String>> announceList = null;
+                    if (!backupURLs.isEmpty()) {
+                        // BEP 12 - Put primary first, then the others, each as the sole entry in their own list
+                        if (announceURL == null) {
+                            _manager.addMessage(_("Error - Cannot include alternate trackers without a primary tracker"));
+                            return;
+                        }
+                        backupURLs.add(0, announceURL);
+                        boolean hasPrivate = false;
+                        boolean hasPublic = false;
+                        for (String url : backupURLs) {
+                            if (_manager.getPrivateTrackers().contains(announceURL))
+                                hasPrivate = true;
+                            else
+                                hasPublic = true;
+                        }
+                        if (hasPrivate && hasPublic) {
+                            _manager.addMessage(_("Error - Cannot mix private and public trackers in a torrent"));
+                            return;
+                        }
+                        announceList = new ArrayList(backupURLs.size());
+                        for (String url : backupURLs) {
+                            announceList.add(Collections.singletonList(url));
+                        }
+                    }
                     try {
                         // This may take a long time to check the storage, but since it already exists,
                         // it shouldn't be THAT bad, so keep it in this thread.
                         // TODO thread it for big torrents, perhaps a la FetchAndAdd
                         boolean isPrivate = _manager.getPrivateTrackers().contains(announceURL);
-                        Storage s = new Storage(_manager.util(), baseFile, announceURL, isPrivate, null);
+                        Storage s = new Storage(_manager.util(), baseFile, announceURL, announceList, isPrivate, null);
                         s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                         MetaInfo info = s.getMetaInfo();
                         File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
@@ -1372,6 +1408,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     }
 
     /**
+     *  Start of anchor only, caller must add anchor text or img and close anchor
      *  @return string or null
      *  @since 0.8.4
      */
@@ -1399,6 +1436,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     }
 
     /**
+     *  Full anchor with img
      *  @return string or null
      *  @since 0.8.4
      */
@@ -1414,6 +1452,29 @@ public class I2PSnarkServlet extends DefaultServlet {
         return null;
     }
 
+    /**
+     *  Full anchor with shortened URL as anchor text
+     *  @return string, non-null
+     *  @since 0.9.5
+     */
+    private String getShortTrackerLink(String announce, byte[] infohash) {
+        StringBuilder buf = new StringBuilder(128);
+        String trackerLinkUrl = getTrackerLinkUrl(announce, infohash);
+        if (trackerLinkUrl != null)
+            buf.append(trackerLinkUrl);
+        if (announce.startsWith("http://"))
+            announce = announce.substring(7);
+        int slsh = announce.indexOf('/');
+        if (slsh > 0)
+            announce = announce.substring(0, slsh);
+        if (announce.length() > 67)
+            announce = announce.substring(0, 40) + "&hellip;" + announce.substring(announce.length() - 8);
+        buf.append(announce);
+        if (trackerLinkUrl != null)
+            buf.append("</a>");
+        return buf.toString();
+    }
+
     private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
         // display incoming parameter if a GET so links will work
         String newURL = req.getParameter("newURL");
@@ -1482,23 +1543,32 @@ public class I2PSnarkServlet extends DefaultServlet {
                   + "\" title=\"");
         out.write(_("File or directory to seed (must be within the specified path)"));
         out.write("\" ><tr><td>\n");
-        out.write(_("Tracker"));
-        out.write(":<td><select name=\"announceURL\"><option value=\"\">");
-        out.write(_("Select a tracker"));
-        out.write("</option>\n");
-        // todo remember this one with _lastAnnounceURL also
-        out.write("<option value=\"none\">");
-        //out.write(_("Open trackers and DHT only"));
-        out.write(_("Open trackers only"));
-        out.write("</option>\n");
+        out.write(_("Trackers"));
+        out.write(":<td><table style=\"width: 20%;\"><tr><td></td><td align=\"center\">");
+        out.write(_("Primary"));
+        out.write("</td><td align=\"center\">");
+        out.write(_("Alternates"));
+        out.write("</td></tr>\n");
         for (Tracker t : sortedTrackers) {
             String name = t.name;
             String announceURL = t.announceURL.replace("&#61;", "=");
+            out.write("<tr><td>");
+            out.write(name);
+            out.write("</td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"");
+            out.write(announceURL);
+            out.write("\"");
             if (announceURL.equals(_lastAnnounceURL))
-                announceURL += "\" selected=\"selected";
-            out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
+                out.write(" checked");
+            out.write("></td><td align=\"center\"><input type=\"checkbox\" name=\"backup_");
+            out.write(announceURL);
+            out.write("\" value=\"foo\"></td></tr>\n");
         }
-        out.write("</select>\n");
+        out.write("<tr><td>");
+        out.write(_("none"));
+        out.write("</td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"none\"");
+        if (_lastAnnounceURL == null)
+            out.write(" checked");
+        out.write("></td><td></td></tr></table>\n");
         // make the user add a tracker on the config form now
         //out.write(_("or"));
         //out.write("&nbsp;<input type=\"text\" name=\"announceURLOther\" size=\"57\" value=\"http://\" " +
@@ -1998,20 +2068,26 @@ public class I2PSnarkServlet extends DefaultServlet {
                     String trackerLink = getTrackerLink(announce, snark.getInfoHash());
                     if (trackerLink != null)
                         buf.append(trackerLink).append(' ');
-                    buf.append("<b>").append(_("Tracker")).append(":</b> ");
-                    String trackerLinkUrl = getTrackerLinkUrl(announce, snark.getInfoHash());
-                    if (trackerLinkUrl != null)
-                        buf.append(trackerLinkUrl);
-                    if (announce.startsWith("http://"))
-                        announce = announce.substring(7);
-                    int slsh = announce.indexOf('/');
-                    if (slsh > 0)
-                        announce = announce.substring(0, slsh);
-                    if (announce.length() > 67)
-                        announce = announce.substring(0, 40) + "&hellip;" + announce.substring(announce.length() - 8);
-                    buf.append(announce);
-                    if (trackerLinkUrl != null)
-                        buf.append("</a>");
+                    buf.append("<b>").append(_("Primary Tracker")).append(":</b> ");
+                    buf.append(getShortTrackerLink(announce, snark.getInfoHash()));
+                    buf.append("</td></tr>");
+                }
+                List<List<String>> alist = meta.getAnnounceList();
+                if (alist != null) {
+                    buf.append("<tr><td><b>");
+                    buf.append(_("Tracker List")).append(":</b> ");
+                    for (List<String> alist2 : alist) {
+                        buf.append('[');
+                        boolean more = false;
+                        for (String s : alist2) {
+                            if (more)
+                                buf.append(' ');
+                            else
+                                more = true;
+                            buf.append(getShortTrackerLink(s, snark.getInfoHash()));
+                        }
+                        buf.append("] ");
+                    }
                     buf.append("</td></tr>");
                 }
             }
-- 
GitLab