forked from I2P_Developers/i2p.i2p
- i2psnark: Add announce list support (BEP 12) (ticket #778)
Preliminary. Still todo: TrackerClient
This commit is contained in:
@ -61,6 +61,7 @@ public class MetaInfo
|
|||||||
private final byte[] piece_hashes;
|
private final byte[] piece_hashes;
|
||||||
private final long length;
|
private final long length;
|
||||||
private final boolean privateTorrent;
|
private final boolean privateTorrent;
|
||||||
|
private final List<List<String>> announce_list;
|
||||||
private Map<String, BEValue> infoMap;
|
private Map<String, BEValue> infoMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,9 +70,11 @@ public class MetaInfo
|
|||||||
* @param announce may be null
|
* @param announce may be null
|
||||||
* @param files null for single-file torrent
|
* @param files null for single-file torrent
|
||||||
* @param lengths 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,
|
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.announce = announce;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -83,6 +86,7 @@ public class MetaInfo
|
|||||||
this.piece_hashes = piece_hashes;
|
this.piece_hashes = piece_hashes;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
this.privateTorrent = privateTorrent;
|
this.privateTorrent = privateTorrent;
|
||||||
|
this.announce_list = announce_list;
|
||||||
|
|
||||||
// TODO if we add a parameter for other keys
|
// TODO if we add a parameter for other keys
|
||||||
//if (other != null) {
|
//if (other != null) {
|
||||||
@ -141,6 +145,23 @@ public class MetaInfo
|
|||||||
this.announce = val.getString();
|
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");
|
val = m.get("info");
|
||||||
if (val == null)
|
if (val == null)
|
||||||
throw new InvalidBEncodingException("Missing info map");
|
throw new InvalidBEncodingException("Missing info map");
|
||||||
@ -296,6 +317,15 @@ public class MetaInfo
|
|||||||
return announce;
|
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.
|
* 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
|
* Creates a copy of this MetaInfo that shares everything except the
|
||||||
* announce URL.
|
* announce URL.
|
||||||
|
* Drops any announce-list.
|
||||||
*/
|
*/
|
||||||
public MetaInfo reannounce(String announce)
|
public MetaInfo reannounce(String announce)
|
||||||
{
|
{
|
||||||
return new MetaInfo(announce, name, name_utf8, files,
|
return new MetaInfo(announce, name, name_utf8, files,
|
||||||
lengths, piece_length,
|
lengths, piece_length,
|
||||||
piece_hashes, length, privateTorrent);
|
piece_hashes, length, privateTorrent, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -486,6 +517,8 @@ public class MetaInfo
|
|||||||
Map m = new HashMap();
|
Map m = new HashMap();
|
||||||
if (announce != null)
|
if (announce != null)
|
||||||
m.put("announce", announce);
|
m.put("announce", announce);
|
||||||
|
if (announce_list != null)
|
||||||
|
m.put("announce-list", announce_list);
|
||||||
Map info = createInfoMap();
|
Map info = createInfoMap();
|
||||||
m.put("info", info);
|
m.put("info", info);
|
||||||
// don't save this locally, we should only do this once
|
// don't save this locally, we should only do this once
|
||||||
|
@ -886,7 +886,9 @@ public class SnarkManager implements CompleteListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException ioe) {
|
} 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())
|
if (sfile.exists())
|
||||||
sfile.delete();
|
sfile.delete();
|
||||||
return;
|
return;
|
||||||
|
@ -122,6 +122,7 @@ public class Storage
|
|||||||
* @throws IOException when creating and/or checking files fails.
|
* @throws IOException when creating and/or checking files fails.
|
||||||
*/
|
*/
|
||||||
public Storage(I2PSnarkUtil util, File baseFile, String announce,
|
public Storage(I2PSnarkUtil util, File baseFile, String announce,
|
||||||
|
List<List<String>> announce_list,
|
||||||
boolean privateTorrent, StorageListener listener)
|
boolean privateTorrent, StorageListener listener)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
@ -182,7 +183,8 @@ public class Storage
|
|||||||
// TODO thread this so we can return and show something on the UI
|
// TODO thread this so we can return and show something on the UI
|
||||||
byte[] piece_hashes = fast_digestCreate();
|
byte[] piece_hashes = fast_digestCreate();
|
||||||
metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
|
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;
|
File file = null;
|
||||||
FileOutputStream out = null;
|
FileOutputStream out = null;
|
||||||
try {
|
try {
|
||||||
Storage storage = new Storage(util, base, announce, false, null);
|
Storage storage = new Storage(util, base, announce, null, false, null);
|
||||||
MetaInfo meta = storage.getMetaInfo();
|
MetaInfo meta = storage.getMetaInfo();
|
||||||
file = new File(storage.getBaseName() + ".torrent");
|
file = new File(storage.getBaseName() + ".torrent");
|
||||||
out = new FileOutputStream(file);
|
out = new FileOutputStream(file);
|
||||||
|
@ -61,7 +61,7 @@ public class I2PSnarkServlet extends DefaultServlet {
|
|||||||
private Resource _resourceBase;
|
private Resource _resourceBase;
|
||||||
private String _themePath;
|
private String _themePath;
|
||||||
private String _imgPath;
|
private String _imgPath;
|
||||||
private String _lastAnnounceURL = "";
|
private String _lastAnnounceURL;
|
||||||
|
|
||||||
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
|
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()) )
|
//if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
|
||||||
// announceURL = announceURLOther;
|
// announceURL = announceURLOther;
|
||||||
|
|
||||||
if (announceURL == null || announceURL.length() <= 0)
|
if (baseFile.exists()) {
|
||||||
_manager.addMessage(_("Error creating torrent - you must select a tracker"));
|
|
||||||
else if (baseFile.exists()) {
|
|
||||||
_lastAnnounceURL = announceURL;
|
|
||||||
if (announceURL.equals("none"))
|
if (announceURL.equals("none"))
|
||||||
announceURL = null;
|
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 {
|
try {
|
||||||
// This may take a long time to check the storage, but since it already exists,
|
// 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.
|
// it shouldn't be THAT bad, so keep it in this thread.
|
||||||
// TODO thread it for big torrents, perhaps a la FetchAndAdd
|
// TODO thread it for big torrents, perhaps a la FetchAndAdd
|
||||||
boolean isPrivate = _manager.getPrivateTrackers().contains(announceURL);
|
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
|
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
|
||||||
MetaInfo info = s.getMetaInfo();
|
MetaInfo info = s.getMetaInfo();
|
||||||
File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
|
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
|
* @return string or null
|
||||||
* @since 0.8.4
|
* @since 0.8.4
|
||||||
*/
|
*/
|
||||||
@ -1399,6 +1436,7 @@ public class I2PSnarkServlet extends DefaultServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Full anchor with img
|
||||||
* @return string or null
|
* @return string or null
|
||||||
* @since 0.8.4
|
* @since 0.8.4
|
||||||
*/
|
*/
|
||||||
@ -1414,6 +1452,29 @@ public class I2PSnarkServlet extends DefaultServlet {
|
|||||||
return null;
|
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 {
|
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
|
||||||
// display incoming parameter if a GET so links will work
|
// display incoming parameter if a GET so links will work
|
||||||
String newURL = req.getParameter("newURL");
|
String newURL = req.getParameter("newURL");
|
||||||
@ -1482,23 +1543,32 @@ public class I2PSnarkServlet extends DefaultServlet {
|
|||||||
+ "\" title=\"");
|
+ "\" title=\"");
|
||||||
out.write(_("File or directory to seed (must be within the specified path)"));
|
out.write(_("File or directory to seed (must be within the specified path)"));
|
||||||
out.write("\" ><tr><td>\n");
|
out.write("\" ><tr><td>\n");
|
||||||
out.write(_("Tracker"));
|
out.write(_("Trackers"));
|
||||||
out.write(":<td><select name=\"announceURL\"><option value=\"\">");
|
out.write(":<td><table style=\"width: 20%;\"><tr><td></td><td align=\"center\">");
|
||||||
out.write(_("Select a tracker"));
|
out.write(_("Primary"));
|
||||||
out.write("</option>\n");
|
out.write("</td><td align=\"center\">");
|
||||||
// todo remember this one with _lastAnnounceURL also
|
out.write(_("Alternates"));
|
||||||
out.write("<option value=\"none\">");
|
out.write("</td></tr>\n");
|
||||||
//out.write(_("Open trackers and DHT only"));
|
|
||||||
out.write(_("Open trackers only"));
|
|
||||||
out.write("</option>\n");
|
|
||||||
for (Tracker t : sortedTrackers) {
|
for (Tracker t : sortedTrackers) {
|
||||||
String name = t.name;
|
String name = t.name;
|
||||||
String announceURL = t.announceURL.replace("=", "=");
|
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))
|
if (announceURL.equals(_lastAnnounceURL))
|
||||||
announceURL += "\" selected=\"selected";
|
out.write(" checked");
|
||||||
out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
|
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
|
// make the user add a tracker on the config form now
|
||||||
//out.write(_("or"));
|
//out.write(_("or"));
|
||||||
//out.write(" <input type=\"text\" name=\"announceURLOther\" size=\"57\" value=\"http://\" " +
|
//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());
|
String trackerLink = getTrackerLink(announce, snark.getInfoHash());
|
||||||
if (trackerLink != null)
|
if (trackerLink != null)
|
||||||
buf.append(trackerLink).append(' ');
|
buf.append(trackerLink).append(' ');
|
||||||
buf.append("<b>").append(_("Tracker")).append(":</b> ");
|
buf.append("<b>").append(_("Primary Tracker")).append(":</b> ");
|
||||||
String trackerLinkUrl = getTrackerLinkUrl(announce, snark.getInfoHash());
|
buf.append(getShortTrackerLink(announce, snark.getInfoHash()));
|
||||||
if (trackerLinkUrl != null)
|
buf.append("</td></tr>");
|
||||||
buf.append(trackerLinkUrl);
|
}
|
||||||
if (announce.startsWith("http://"))
|
List<List<String>> alist = meta.getAnnounceList();
|
||||||
announce = announce.substring(7);
|
if (alist != null) {
|
||||||
int slsh = announce.indexOf('/');
|
buf.append("<tr><td><b>");
|
||||||
if (slsh > 0)
|
buf.append(_("Tracker List")).append(":</b> ");
|
||||||
announce = announce.substring(0, slsh);
|
for (List<String> alist2 : alist) {
|
||||||
if (announce.length() > 67)
|
buf.append('[');
|
||||||
announce = announce.substring(0, 40) + "…" + announce.substring(announce.length() - 8);
|
boolean more = false;
|
||||||
buf.append(announce);
|
for (String s : alist2) {
|
||||||
if (trackerLinkUrl != null)
|
if (more)
|
||||||
buf.append("</a>");
|
buf.append(' ');
|
||||||
|
else
|
||||||
|
more = true;
|
||||||
|
buf.append(getShortTrackerLink(s, snark.getInfoHash()));
|
||||||
|
}
|
||||||
|
buf.append("] ");
|
||||||
|
}
|
||||||
buf.append("</td></tr>");
|
buf.append("</td></tr>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user