Compare commits

...

2 Commits

Author SHA1 Message Date
zzz
9a537e2207 Edit torrent UI fixes
after review by drzed. More to follow.
2021-11-25 09:51:32 -05:00
zzz
43fd47cc7f WIP: Torrent editor 2021-11-24 09:59:16 -05:00
3 changed files with 328 additions and 21 deletions

View File

@@ -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<List<String>> files, List<Long> lengths,
public 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,
List<List<String>> announce_list, String created_by, List<String> 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;
}

View File

@@ -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

View File

@@ -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<String, String[]> postParams, String sortParam)
throws IOException
private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> 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("<form action=\"").append(base).append("\" method=\"POST\">\n" +
"<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
"<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\">\n");
if (sortParam != null) {
buf.append("<input type=\"hidden\" name=\"sort\" value=\"")
.append(DataHelper.stripHTML(sortParam)).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("</td><td><b>")
.append(_t("Comment")).append("</b></td><td>")
.append(DataHelper.stripHTML(com))
.append(DataHelper.escapeHTML(com))
.append("</td></tr>\n");
}
long dat = meta.getCreationDate();
@@ -3239,7 +3248,7 @@ public class I2PSnarkServlet extends BasicServlet {
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Created By")).append("</b></td><td>")
.append(DataHelper.stripHTML(cby))
.append(DataHelper.escapeHTML(cby))
.append("</td></tr>\n");
}
long[] dates = _manager.getSavedAddedAndCompleted(snark);
@@ -3390,7 +3399,7 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("<b>").append(_t("Starting")).append("&hellip;</b>");
} else if (snark.isAllocating()) {
buf.append("<b>").append(_t("Allocating")).append("&hellip;</b>");
} else {
} else if (isTopLevel && !showEdit) {
boolean isRunning = !snark.isStopped();
buf.append("<input type=\"submit\" value=\"");
if (isRunning)
@@ -3398,14 +3407,23 @@ public class I2PSnarkServlet extends BasicServlet {
else
buf.append(_t("Start")).append("\" name=\"start\" class=\"starttorrent\">\n");
buf.append("<input type=\"submit\" name=\"recheck\" value=\"").append(_t("Force Recheck"));
if (isRunning)
if (isRunning) {
buf.append("\" class=\"disabled\" disabled=\"disabled\" title=\"")
.append(_t("Stop the torrent in order to check file integrity"))
.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" +
"<input type=\"submit\" name=\"showEdit\" value=\"").append(_t("Edit Torrent"));
if (isRunning) {
buf.append("\" class=\"disabled\" disabled=\"disabled\" title=\"")
.append(_t("Torrent must be stopped"));
} else {
buf.append("\" class=\"reload\" title=\"")
.append(_t("Add or remove trackers"));
}
buf.append("\">\n");
}
boolean showInOrder = storage != null && !storage.complete() &&
meta != null;
@@ -3439,6 +3457,13 @@ public class I2PSnarkServlet extends BasicServlet {
}
buf.append("</table>\n");
if (snark != null && isTopLevel && showEdit) {
// Edit torrent. Show edit section only.
displayTorrentEdit(snark, base, buf);
buf.append("</form></div></div></center></body></html>");
return buf.toString();
}
if (snark != null && !r.exists()) {
// fixup TODO
buf.append("<table class=\"resourceError\" id=\"DoesNotExist\"><tr><th colspan=\"2\">")
@@ -3481,7 +3506,7 @@ public class I2PSnarkServlet extends BasicServlet {
displayComments(snark, er, ec, esc, buf);
if (includeForm)
buf.append("</form>");
buf.append("</div></div></body></html>");
buf.append("</div></div></center></body></html>");
return buf.toString();
}
@@ -3778,7 +3803,7 @@ public class I2PSnarkServlet extends BasicServlet {
// for stop/start/check
if (includeForm)
buf.append("</form>");
buf.append("</div></div></body></html>");
buf.append("</div></div></center></body></html>");
return buf.toString();
}
@@ -4150,6 +4175,134 @@ public class I2PSnarkServlet extends BasicServlet {
buf.append("</div>");
}
/**
* @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("<div id=\"snarkCommentSection\"><table class=\"snarkTorrentInfo\">\n<tr><th colspan=\"5\">")
.append(_t("Edit Torrent"))
.append("</th></tr>");
boolean isRunning = !snark.isStopped();
if (isRunning) {
// shouldn't happen
buf.append("<tr><td colspan=\"5\">")
.append(_t("Torrent must be stopped"))
.append("</td></tr></table></div></form>");
return;
}
String announce = meta.getAnnounce();
if (announce == null)
announce = snark.getTrackerURL();
if (announce != null) {
// strip non-i2p trackers
if (!isI2PTracker(announce))
announce = null;
}
List<List<String>> alist = meta.getAnnounceList();
Set<String> annlist = new TreeSet<String>();
if (alist != null && !alist.isEmpty()) {
// strip non-i2p trackers
for (List<String> alist2 : alist) {
for (String s : alist2) {
if (isI2PTracker(s))
annlist.add(s);
}
}
}
if (announce != null)
annlist.add(announce);
if (!annlist.isEmpty()) {
buf.append("<tr><td colspan=\"3\"></td><td>").append("Primary").append("</td><td>")
.append("Delete").append("</td></tr>");
for (String s : annlist) {
int hc = s.hashCode();
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Tracker")).append("</b></td><td>");
s = DataHelper.stripHTML(s);
buf.append("<span class=\"info_tracker\">");
buf.append(getShortTrackerLink(s, snark.getInfoHash()));
buf.append("</span> ");
//buf.append(s);
buf.append("</td><td>");
buf.append("<input type=\"radio\" class=\"optbox\" name=\"primary\" ");
if (s.equals(announce))
buf.append("checked=\"checked\" ");
buf.append("value=\"").append(hc);
buf.append("\"></td><td>");
buf.append("<input type=\"checkbox\" class=\"optbox\" name=\"trdelete.")
.append(hc).append("\" title=\"").append(_t("Mark for deletion")).append("\">");
buf.append("</td></tr>\n");
}
}
List<Tracker> newTrackers = _manager.getSortedTrackers();
for (Iterator<Tracker> iter = newTrackers.iterator(); iter.hasNext(); ) {
Tracker t = iter.next();
String announceURL = t.announceURL.replace("&#61;", "=");
if (announceURL.equals(announce) || annlist.contains(announceURL))
iter.remove();
}
if (!newTrackers.isEmpty()) {
buf.append("<tr><td colspan=\"3\"></td><td>").append("Primary").append("</td><td>")
.append("Add").append("</td></tr>");
for (Tracker t : newTrackers) {
String name = t.name;
int hc = t.announceURL.hashCode();
String announceURL = t.announceURL.replace("&#61;", "=");
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Add Tracker")).append("</b></td><td>");
buf.append(name);
buf.append("</td><td><input type=\"radio\" class=\"optbox\" name=\"primary\" value=\"");
buf.append(hc);
buf.append("\"></td><td>");
buf.append("<input type=\"checkbox\" class=\"optbox\" id=\"").append(name).append("\" name=\"tradd.")
.append(hc).append("\" title=\"").append(_t("Add tracker")).append("\"> ")
.append("</td><td></td></tr>\n");
}
}
String com = meta.getComment();
if (com == null) {
com = "";
} else if (com.length() > 0) {
com = DataHelper.escapeHTML(com);
}
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Comment")).append("</b></td>");
buf.append("<td colspan=\"2\" id=\"addCommentText\"><textarea name=\"nofilter_newTorrentComment\" cols=\"88\" rows=\"4\">")
.append(com).append("</textarea></td><td></td>");
buf.append("</tr>\n");
String cb = meta.getCreatedBy();
if (cb == null) {
cb = "";
} else if (cb.length() > 0) {
cb = DataHelper.escapeHTML(cb);
}
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Created By")).append("</b></td>");
buf.append("<td id=\"editTorrentCreatedBy\"><input type=\"text\" name=\"nofilter_newTorrentCreatedBy\" cols=\"44\" rows=\"1\" value=\"")
.append(cb).append("\"></td></tr>");
buf.append("<tr id=\"torrentInfoControl\"><td colspan=\"5\">");
buf.append("<input type=\"submit\" name=\"editTorrent\" value=\"");
buf.append(_t("Save Changes"));
buf.append("\" class=\"accept\"></td></tr>\n");
buf.append("</table></div>");
}
/**
* @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<String, String[]> postParams) {
if (!snark.isStopped()) {
// shouldn't happen
_manager.addMessage(_t("Torrent must be stopped"));
return;
}
List<Integer> toAdd = new ArrayList<Integer>();
List<Integer> toDel = new ArrayList<Integer>();
Integer primary = null;
String newComment = "";
String newCreatedBy = "";
for (Map.Entry<String, String[]> 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<List<String>> alist = meta.getAnnounceList();
Set<String> annlist = new TreeSet<String>();
if (alist != null && !alist.isEmpty()) {
// strip non-i2p trackers
for (List<String> alist2 : alist) {
for (String s : alist2) {
if (isI2PTracker(s))
annlist.add(s);
}
}
}
if (oldPrimary != null)
annlist.add(oldPrimary);
List<Tracker> newTrackers = _manager.getSortedTrackers();
for (Integer i : toDel) {
int hc = i.intValue();
for (Iterator<String> 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<List<String>> newAnnList;
if (annlist.isEmpty()) {
newAnnList = null;
thePrimary = null;
} else {
List<String> aalist = new ArrayList<String>(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...