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) + "…" + 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("=", "="); + 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(" <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) + "…" + 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