From 2a900a8c5bb4099532e3f4dc79fcf9a1e8ea1f91 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 3 Dec 2021 06:26:14 -0500 Subject: [PATCH] i2psnark: Add torrent edit page Additional UI cleanup to follow --- .../java/src/org/klomp/snark/MetaInfo.java | 10 +- .../java/src/org/klomp/snark/Snark.java | 9 + .../org/klomp/snark/web/I2PSnarkServlet.java | 330 +++++++++++++++++- 3 files changed, 328 insertions(+), 21 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java index c191a8220..e0f412297 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java @@ -82,8 +82,9 @@ public class MetaInfo * @param created_by may be null * @param url_list may be null * @param comment may be null + * @since public since 0.9.53, was package private */ - MetaInfo(String announce, String name, String name_utf8, List> files, List lengths, + public MetaInfo(String announce, String name, String name_utf8, List> files, List lengths, int piece_length, byte[] piece_hashes, long length, boolean privateTorrent, List> announce_list, String created_by, List url_list, String comment) { @@ -442,9 +443,12 @@ public class MetaInfo } /** - * Returns the piece hashes. Only used by storage so package local. + * Returns the piece hashes. + * + * @return not a copy, do not modify + * @since public since 0.9.53, was package private */ - byte[] getPieceHashes() + public byte[] getPieceHashes() { return piece_hashes; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index b95e9efab..d669a674d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -1321,6 +1321,15 @@ public class Snark } } + /** + * Call after editing torrent. + * Caller must ensure infohash, files, etc. did not change. + * + * @since 0.9.53 + */ + public void replaceMetaInfo(MetaInfo metainfo) { + meta = metainfo; + } ///////////// Begin StorageListener methods 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 bf641e394..d25205c1c 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -3,6 +3,7 @@ package org.klomp.snark.web; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; @@ -22,6 +23,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -35,8 +37,10 @@ import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.servlet.util.ServletUtil; +import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.SecureFile; +import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; import net.i2p.util.Translate; import net.i2p.util.UIMessages; @@ -251,11 +255,12 @@ public class I2PSnarkServlet extends BasicServlet { } } else { String base = addPaths(req.getRequestURI(), "/"); + boolean showEdit = req.getParameter("showEdit") != null; String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null, - req.getParameter("sort")); + req.getParameter("sort"), showEdit); if (method.equals("POST")) { // P-R-G - sendRedirect(req, resp, ""); + sendRedirect(req, resp, showEdit ? "?showEdit" : ""); } else if (listing != null) { setHTMLHeaders(resp, cspNonce, true); resp.getWriter().write(listing); @@ -2984,8 +2989,8 @@ public class I2PSnarkServlet extends BasicServlet { * @return String of HTML or null if postParams != null * @since 0.7.14 */ - private String getListHTML(File xxxr, String base, boolean parent, Map postParams, String sortParam) - throws IOException + private String getListHTML(File xxxr, String base, boolean parent, Map postParams, + String sortParam, boolean showEdit) throws IOException { String decodedBase = decodePath(base); String title = decodedBase; @@ -3027,6 +3032,10 @@ public class I2PSnarkServlet extends BasicServlet { _manager.startTorrent(snark); } else if (postParams.get("recheck") != null) { _manager.recheckTorrent(snark); + } else if (postParams.get("editTorrent") != null) { + saveTorrentEdit(snark, postParams); + } else if (postParams.get("showEdit") != null) { + // P-R-G only } else { _manager.addMessage("Unknown command"); } @@ -3055,7 +3064,7 @@ public class I2PSnarkServlet extends BasicServlet { r = new File(""); } - boolean showStopStart = snark != null; + boolean showStopStart = snark != null && !showEdit; Storage storage = snark != null ? snark.getStorage() : null; boolean showPriority = storage != null && !storage.complete() && r.isDirectory(); @@ -3093,7 +3102,7 @@ public class I2PSnarkServlet extends BasicServlet { final boolean includeForm = showStopStart || showPriority || er || ec; if (includeForm) { buf.append("
\n" + - "\n"); + "\n"); if (sortParam != null) { buf.append("\n"); @@ -3137,7 +3146,7 @@ public class I2PSnarkServlet extends BasicServlet { String announce = null; MetaInfo meta = snark.getMetaInfo(); - if (meta != null) { + if (meta != null && !showEdit) { announce = meta.getAnnounce(); if (announce == null) announce = snark.getTrackerURL(); @@ -3218,7 +3227,7 @@ public class I2PSnarkServlet extends BasicServlet { toThemeImg(buf, "details"); buf.append("") .append(_t("Comment")).append("") - .append(DataHelper.stripHTML(com)) + .append(DataHelper.escapeHTML(com)) .append("\n"); } long dat = meta.getCreationDate(); @@ -3239,7 +3248,7 @@ public class I2PSnarkServlet extends BasicServlet { toThemeImg(buf, "details"); buf.append("") .append(_t("Created By")).append("") - .append(DataHelper.stripHTML(cby)) + .append(DataHelper.escapeHTML(cby)) .append("\n"); } long[] dates = _manager.getSavedAddedAndCompleted(snark); @@ -3390,7 +3399,7 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("").append(_t("Starting")).append("…"); } else if (snark.isAllocating()) { buf.append("").append(_t("Allocating")).append("…"); - } else { + } else if (isTopLevel && !showEdit) { boolean isRunning = !snark.isStopped(); buf.append("\n"); buf.append("\n"); - else + .append(_t("Torrent must be stopped")); + } else { buf.append("\" class=\"reload\" title=\"") - .append(_t("Check integrity of the downloaded files")) - .append("\">\n"); + .append(_t("Check integrity of the downloaded files")); + } + buf.append("\">\n" + + "\n"); } boolean showInOrder = storage != null && !storage.complete() && meta != null; @@ -3439,6 +3457,13 @@ public class I2PSnarkServlet extends BasicServlet { } buf.append("\n"); + if (snark != null && isTopLevel && showEdit) { + // Edit torrent. Show edit section only. + displayTorrentEdit(snark, base, buf); + buf.append("
"); + return buf.toString(); + } + if (snark != null && !r.exists()) { // fixup TODO buf.append(""); + for (String s : annlist) { + int hc = s.hashCode(); + buf.append("\n"); + } + } + + List newTrackers = _manager.getSortedTrackers(); + for (Iterator iter = newTrackers.iterator(); iter.hasNext(); ) { + Tracker t = iter.next(); + String announceURL = t.announceURL.replace("=", "="); + if (announceURL.equals(announce) || annlist.contains(announceURL)) + iter.remove(); + } + if (!newTrackers.isEmpty()) { + buf.append(""); + for (Tracker t : newTrackers) { + String name = t.name; + int hc = t.announceURL.hashCode(); + String announceURL = t.announceURL.replace("=", "="); + buf.append("\n"); + } + } + + String com = meta.getComment(); + if (com == null) { + com = ""; + } else if (com.length() > 0) { + com = DataHelper.escapeHTML(com); + } + buf.append(""); + buf.append(""); + buf.append("\n"); + + String cb = meta.getCreatedBy(); + if (cb == null) { + cb = ""; + } else if (cb.length() > 0) { + cb = DataHelper.escapeHTML(cb); + } + buf.append(""); + buf.append(""); + + buf.append("\n"); + buf.append("
") @@ -3481,7 +3506,7 @@ public class I2PSnarkServlet extends BasicServlet { displayComments(snark, er, ec, esc, buf); if (includeForm) buf.append(""); - buf.append(""); + buf.append(""); return buf.toString(); } @@ -3778,7 +3803,7 @@ public class I2PSnarkServlet extends BasicServlet { // for stop/start/check if (includeForm) buf.append(""); - buf.append(""); + buf.append(""); return buf.toString(); } @@ -4150,6 +4175,134 @@ public class I2PSnarkServlet extends BasicServlet { buf.append(""); } + /** + * @param snark non-null + * @since 0.9.53 + */ + private void displayTorrentEdit(Snark snark, String base, StringBuilder buf) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) + return; + buf.append("
\n"); + boolean isRunning = !snark.isStopped(); + if (isRunning) { + // shouldn't happen + buf.append("
") + .append(_t("Edit Torrent")) + .append("
") + .append(_t("Torrent must be stopped")) + .append("
"); + return; + } + String announce = meta.getAnnounce(); + if (announce == null) + announce = snark.getTrackerURL(); + if (announce != null) { + // strip non-i2p trackers + if (!isI2PTracker(announce)) + announce = null; + } + List> alist = meta.getAnnounceList(); + Set annlist = new TreeSet(); + if (alist != null && !alist.isEmpty()) { + // strip non-i2p trackers + for (List alist2 : alist) { + for (String s : alist2) { + if (isI2PTracker(s)) + annlist.add(s); + } + } + } + if (announce != null) + annlist.add(announce); + if (!annlist.isEmpty()) { + buf.append("
").append("Primary").append("") + .append("Delete").append("
"); + toThemeImg(buf, "details"); + buf.append("") + .append(_t("Tracker")).append(""); + s = DataHelper.stripHTML(s); + buf.append(""); + buf.append(getShortTrackerLink(s, snark.getInfoHash())); + buf.append(" "); + //buf.append(s); + buf.append(""); + buf.append(""); + buf.append(""); + buf.append("
").append("Primary").append("") + .append("Add").append("
"); + toThemeImg(buf, "details"); + buf.append("") + .append(_t("Add Tracker")).append(""); + buf.append(name); + buf.append(""); + buf.append(" ") + .append("
"); + toThemeImg(buf, "details"); + buf.append("") + .append(_t("Comment")).append("
"); + toThemeImg(buf, "details"); + buf.append("") + .append(_t("Created By")).append("
"); + buf.append("
"); + } + /** * @param so null ok * @return query string or "" @@ -4370,6 +4523,147 @@ public class I2PSnarkServlet extends BasicServlet { _manager.setSavedCommentsEnabled(snark, yes); } + /** + * @since 0.9.53 + */ + private void saveTorrentEdit(Snark snark, Map postParams) { + if (!snark.isStopped()) { + // shouldn't happen + _manager.addMessage(_t("Torrent must be stopped")); + return; + } + List toAdd = new ArrayList(); + List toDel = new ArrayList(); + Integer primary = null; + String newComment = ""; + String newCreatedBy = ""; + for (Map.Entry entry : postParams.entrySet()) { + String key = entry.getKey(); + String val = entry.getValue()[0]; // jetty arrays + if (key.startsWith("tradd.")) { + try { + toAdd.add(Integer.parseInt(key.substring(6))); + } catch (NumberFormatException nfe) {} + } else if (key.startsWith("trdelete.")) { + try { + toDel.add(Integer.parseInt(key.substring(9))); + } catch (NumberFormatException nfe) {} + } else if (key.equals("primary")) { + try { + primary = Integer.parseInt(val); + } catch (NumberFormatException nfe) {} + } else if (key.equals("nofilter_newTorrentComment")) { + newComment = val.trim(); + } else if (key.equals("nofilter_newTorrentCreatedBy")) { + newCreatedBy = val.trim(); + } + } + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) { + // shouldn't happen + _manager.addMessage("Can't edit magnet"); + return; + } + String oldPrimary = meta.getAnnounce(); + String oldComment = meta.getComment(); + if (oldComment == null) + oldComment = ""; + String oldCreatedBy = meta.getCreatedBy(); + if (oldCreatedBy == null) + oldCreatedBy = ""; + if (toAdd.isEmpty() && toDel.isEmpty() && + (primary == null || primary.equals(oldPrimary)) && + oldComment.equals(newComment) && + oldCreatedBy.equals(newCreatedBy)) { + _manager.addMessage("No changes to torrent, not saved"); + return; + } + List> alist = meta.getAnnounceList(); + Set annlist = new TreeSet(); + if (alist != null && !alist.isEmpty()) { + // strip non-i2p trackers + for (List alist2 : alist) { + for (String s : alist2) { + if (isI2PTracker(s)) + annlist.add(s); + } + } + } + if (oldPrimary != null) + annlist.add(oldPrimary); + List newTrackers = _manager.getSortedTrackers(); + for (Integer i : toDel) { + int hc = i.intValue(); + for (Iterator iter = annlist.iterator(); iter.hasNext(); ) { + String s = iter.next(); + if (s.hashCode() == hc) + iter.remove(); + } + } + for (Integer i : toAdd) { + int hc = i.intValue(); + for (Tracker t : newTrackers) { + if (t.announceURL.hashCode() == hc) { + annlist.add(t.announceURL); + break; + } + } + } + String thePrimary = oldPrimary; + if (primary != null) { + int hc = primary.intValue(); + for (String s : annlist) { + if (s.hashCode() == hc) { + thePrimary = s; + break; + } + } + } + List> newAnnList; + if (annlist.isEmpty()) { + newAnnList = null; + thePrimary = null; + } else { + List aalist = new ArrayList(annlist); + newAnnList = Collections.singletonList(aalist); + if (!aalist.contains(thePrimary)) + thePrimary = aalist.get(0); + } + if (newComment.equals("")) + newComment = null; + if (newCreatedBy.equals("")) + newCreatedBy = null; + MetaInfo newMeta = new MetaInfo(thePrimary, meta.getName(), null, meta.getFiles(), meta.getLengths(), + meta.getPieceLength(0), meta.getPieceHashes(), meta.getTotalLength(), meta.isPrivate(), + newAnnList, newCreatedBy, meta.getWebSeedURLs(), newComment); + if (!DataHelper.eq(meta.getInfoHash(), newMeta.getInfoHash())) { + // shouldn't happen + _manager.addMessage("Torrent edit failed, infohash mismatch"); + return; + } + File f = new File(_manager.util().getTempDir(), "edit-" + _manager.util().getContext().random().nextLong() + ".torrent"); + OutputStream out = null; + try { + out = new SecureFileOutputStream(f); + out.write(newMeta.getTorrentData()); + out.close(); + boolean ok = FileUtil.rename(f, new File(snark.getName())); + if (!ok) { + _manager.addMessage("Save edit changes failed"); + return; + } + } catch (IOException ioe) { + try { if (out != null) out.close(); } catch (IOException ioe2) {} + _manager.addMessage("Save edit changes failed: " + ioe); + return; + } finally { + f.delete(); + } + snark.replaceMetaInfo(newMeta); + _manager.addMessage("Torrent changes saved"); + } + + /** @since 0.9.32 */ private static boolean noCollapsePanels(HttpServletRequest req) { // check for user agents that can't toggle the collapsible panels...