From 30e2f73d5f3ab68928c9f32c16f027ff236970b4 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 11 Jun 2012 12:04:40 +0000 Subject: [PATCH] * i2psnark: - Display torrent file downloads in torrent area - Sort magnets and downloads first - Fix sorting problem when torrent dir is a symlink - Reduce max file idle time - arrow_down icon copied from console css --- apps/i2psnark/icons/arrow_down.png | Bin 0 -> 379 bytes apps/i2psnark/java/build.xml | 4 +- .../src/org/klomp/snark/I2PSnarkUtil.java | 11 +- .../java/src/org/klomp/snark/Snark.java | 4 + .../src/org/klomp/snark/SnarkManager.java | 27 +- .../java/src/org/klomp/snark/Storage.java | 2 +- .../src/org/klomp/snark/web/FetchAndAdd.java | 346 ++++++++++++++++++ .../org/klomp/snark/web/I2PSnarkServlet.java | 114 ++---- history.txt | 8 + .../src/net/i2p/router/RouterVersion.java | 2 +- 10 files changed, 421 insertions(+), 97 deletions(-) create mode 100644 apps/i2psnark/icons/arrow_down.png create mode 100644 apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java diff --git a/apps/i2psnark/icons/arrow_down.png b/apps/i2psnark/icons/arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2c4e279377bf348f9cf53894e76bb673ccf067bd GIT binary patch literal 379 zcmV->0fhdEP)RB*?~^j!LKVQ>(O&A{Xr%)RXLn#U zs4LtZ6rCMFY5|B2$)yG$6aaIF - + @@ -72,7 +72,7 @@ - + diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index a00ad001e..f4238dae4 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -250,6 +250,15 @@ public class I2PSnarkUtil { public boolean connected() { return _manager != null; } + /** + * For FetchAndAdd + * @return null if not connected + * @since 0.9.1 + */ + public I2PSocketManager getSocketManager() { + return _manager; + } + /** * Destroy the destination itself */ @@ -310,7 +319,7 @@ public class I2PSnarkUtil { // we could use the system tmp dir but deleteOnExit() doesn't seem to work on all platforms... out = SecureFile.createTempFile("i2psnark", null, _tmpDir); } catch (IOException ioe) { - ioe.printStackTrace(); + _log.error("temp file error", ioe); if (out != null) out.delete(); return null; diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 0d8d451a8..f5fe35e31 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -346,6 +346,8 @@ public class Snark in = new FileInputStream(f); else { + /**** No, we don't ever fetch a torrent file this way + and we don't want to block in the constructor activity = "Getting torrent"; File torrentFile = _util.get(torrent, 3); if (torrentFile == null) { @@ -355,6 +357,8 @@ public class Snark torrentFile.deleteOnExit(); in = new FileInputStream(torrentFile); } + *****/ + throw new IOException("not found"); } meta = new MetaInfo(in); infoHash = meta.getInfoHash(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index a71975eac..26023cab5 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -598,7 +598,7 @@ public class SnarkManager implements Snark.CompleteListener { } /** - * @param ot null ok, default is none + * @param pt null ok, default is none * @since 0.9.1 */ public void savePrivateTrackers(List pt) { @@ -881,6 +881,29 @@ public class SnarkManager implements Snark.CompleteListener { _magnets.remove(snark.getName()); removeMagnetStatus(snark.getInfoHash()); } + + /** + * Add and start a FetchAndAdd task. + * Remove it with deleteMagnet(). + * + * @param torrent must be instanceof FetchAndAdd + * @throws RuntimeException via Snark.fatal()? + * @since 0.9.1 + */ + public void addDownloader(Snark torrent) { + synchronized (_snarks) { + Snark snark = getTorrentByInfoHash(torrent.getInfoHash()); + if (snark != null) { + addMessage(_("Download already running: {0}", snark.getBaseName())); + return; + } + String name = torrent.getName(); + // Tell the dir monitor not to delete us + _magnets.add(name); + _snarks.put(name, torrent); + } + torrent.startTorrent(); + } /** * Add a torrent from a MetaInfo. Save the MetaInfo data to filename. @@ -1399,7 +1422,7 @@ public class SnarkManager implements Snark.CompleteListener { byte[] ih = Base64.decode(b64); // ignore value - TODO put tracker URL in value if (ih != null && ih.length == 20) - addMagnet("Magnet: " + I2PSnarkUtil.toHex(ih), ih, null, false); + addMagnet("* " + _("Magnet") + ' ' + I2PSnarkUtil.toHex(ih), ih, null, false); // else remove from config? } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 3c8be6384..0dc5f0958 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -1071,7 +1071,7 @@ public class Storage /** * Close unused RAFs - call periodically */ - private static final long RAFCloseDelay = 7*60*1000; + private static final long RAFCloseDelay = 4*60*1000; public void cleanRAFs() { long cutoff = System.currentTimeMillis() - RAFCloseDelay; for (int i = 0; i < RAFlock.length; i++) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java b/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java new file mode 100644 index 000000000..abf007a53 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java @@ -0,0 +1,346 @@ +package org.klomp.snark.web; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import net.i2p.I2PAppContext; +import net.i2p.client.streaming.I2PSocketEepGet; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.crypto.SHA1; +import net.i2p.util.EepGet; +import net.i2p.util.I2PAppThread; +import net.i2p.util.Log; +import net.i2p.util.SecureFile; + +import org.klomp.snark.MetaInfo; +import org.klomp.snark.Snark; +import org.klomp.snark.SnarkManager; +import org.klomp.snark.Storage; + +/** + * A cancellable torrent file downloader. + * We extend Snark so its status may be easily listed in the + * web table without adding a lot of code there. + * + * Upon successful download, this Snark will be deleted and + * a "real" Snark created. + * + * The methods return values similar to a Snark in magnet mode. + * A fake info hash, which is the SHA1 of the URL, is returned + * to prevent duplicates. + * + * This Snark may be stopped and restarted, although a partially + * downloaded file is discarded. + * + * @since 0.9.1 Moved from I2PSnarkUtil + */ +public class FetchAndAdd extends Snark implements EepGet.StatusListener, Runnable { + + private final I2PAppContext _ctx; + private final Log _log; + private final SnarkManager _mgr; + private final String _url; + private final byte[] _fakeHash; + private final String _name; + private volatile long _remaining = -1; + private volatile long _total = -1; + private volatile long _transferred; + private volatile boolean _isRunning; + private volatile boolean _active; + private volatile long _started; + private String _failCause; + private Thread _thread; + private EepGet _eepGet; + + private static final int RETRIES = 3; + + /** + * Caller should call _mgr.addDownloader(this), which + * will start things off. + */ + public FetchAndAdd(I2PAppContext ctx, SnarkManager mgr, String url) { + // magnet constructor + super(mgr.util(), "Torrent download", + null, null, null, null, null, false, null); + _ctx = ctx; + _log = ctx.logManager().getLog(FetchAndAdd.class); + _mgr = mgr; + _url = url; + _name = "* " + _("Download torrent file from {0}", url); + byte[] fake = null; + try { + fake = SHA1.getInstance().digest(url.getBytes("ISO-8859-1")); + } catch (IOException ioe) {} + _fakeHash = fake; + } + + /** + * Set off by startTorrent() + */ + public void run() { + _mgr.addMessage(_("Fetching {0}", urlify(_url))); + File file = get(); + if (!_isRunning) // stopped? + return; + _isRunning = false; + if (file != null && file.exists() && file.length() > 0) { + // remove this in snarks + _mgr.deleteMagnet(this); + add(file); + } else { + _mgr.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)) + + ((_failCause != null) ? (": " + _failCause) : "")); + } + if (file != null) + file.delete(); + } + + /** + * Copied from I2PSnarkUtil so we may add ourselves as a status listener + * @return null on failure + */ + private File get() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetching [" + _url + "]"); + File out = null; + try { + out = SecureFile.createTempFile("torrentFile", null, _mgr.util().getTempDir()); + } catch (IOException ioe) { + _log.error("temp file error", ioe); + _mgr.addMessage("Temp file error: " + ioe); + if (out != null) + out.delete(); + return null; + } + out.deleteOnExit(); + + if (!_mgr.util().connected()) { + _mgr.addMessage(_("Opening the I2P tunnel")); + if (!_mgr.util().connect()) + return null; + } + I2PSocketManager manager = _mgr.util().getSocketManager(); + if (manager == null) + return null; + _eepGet = new I2PSocketEepGet(_ctx, manager, RETRIES, out.getAbsolutePath(), _url); + _eepGet.addStatusListener(this); + if (_eepGet.fetch()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetch successful [" + _url + "]: size=" + out.length()); + return out; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetch failed [" + _url + ']'); + out.delete(); + return null; + } + } + + /** + * Tell SnarkManager to copy the torrent file over and add it to the Snarks list. + * This Snark may then be deleted. + */ + private void add(File file) { + _mgr.addMessage(_("Torrent fetched from {0}", urlify(_url))); + FileInputStream in = null; + try { + in = new FileInputStream(file); + byte[] fileInfoHash = new byte[20]; + String name = MetaInfo.getNameAndInfoHash(in, fileInfoHash); + try { in.close(); } catch (IOException ioe) {} + Snark snark = _mgr.getTorrentByInfoHash(fileInfoHash); + if (snark != null) { + _mgr.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName())); + return; + } + + name = Storage.filterName(name); + name = name + ".torrent"; + File torrentFile = new File(_mgr.getDataDir(), name); + + String canonical = torrentFile.getCanonicalPath(); + + if (torrentFile.exists()) { + if (_mgr.getTorrent(canonical) != null) + _mgr.addMessage(_("Torrent already running: {0}", name)); + else + _mgr.addMessage(_("Torrent already in the queue: {0}", name)); + } else { + // This may take a LONG time to create the storage. + _mgr.copyAndAddTorrent(file, canonical); + } + } catch (IOException ioe) { + _mgr.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage()); + } catch (OutOfMemoryError oom) { + _mgr.addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + oom.getMessage()); + } finally { + try { if (in != null) in.close(); } catch (IOException ioe) {} + } + } + + // Snark overrides so all the buttons and stats on the web page work + + @Override + public synchronized void startTorrent() { + if (_isRunning) + return; + // reset counters in case starting a second time + _remaining = -1; + // leave the total if we knew it before + //_total = -1; + _transferred = 0; + _failCause = null; + _started = _ctx.clock().now(); + _isRunning = true; + _active = false; + _thread = new I2PAppThread(this, "Torrent File EepGet", true); + _thread.start(); + } + + @Override + public synchronized void stopTorrent() { + if (_thread != null && _isRunning) { + if (_eepGet != null) + _eepGet.stopFetching(); + _thread.interrupt(); + } + _isRunning = false; + _active = false; + } + + @Override + public boolean isStopped() { + return !_isRunning; + } + + @Override + public String getName() { + return _name; + } + + @Override + public String getBaseName() { + return _name; + } + + @Override + public byte[] getInfoHash() { + return _fakeHash; + } + + /** + * @return torrent file size or -1 + */ + @Override + public long getTotalLength() { + return _total; + } + + /** + * @return torrent file bytes remaining or -1 + */ + @Override + public long getRemainingLength() { + return _remaining; + } + + /** + * @return torrent file bytes remaining or -1 + */ + @Override + public long getNeededLength() { + return _remaining; + } + + @Override + public long getDownloadRate() { + if (_isRunning && _active) { + long time = _ctx.clock().now() - _started; + if (time > 1000) { + long rv = (_transferred * 1000) / time; + if (rv >= 100) + return rv; + } + } + return 0; + } + + @Override + public long getDownloaded() { + return _total - _remaining; + } + + @Override + public int getPeerCount() { + return (_isRunning && _active && _transferred > 0) ? 1 : 0; + } + + @Override + public int getTrackerSeenPeers() { + return (_transferred > 0) ? 1 : 0; + } + + // End Snark overrides + + // EepGet status listeners to maintain the state for the web page + + public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { + if (bytesRemaining >= 0) { + _remaining = bytesRemaining; + } + _transferred = bytesTransferred; + if (cause != null) + _failCause = cause.toString(); + _active = false; + } + + public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) { + if (bytesRemaining >= 0) { + _remaining = bytesRemaining; + _total = bytesRemaining + currentWrite + alreadyTransferred; + } + _transferred = bytesTransferred; + _active = true; + } + + public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { + if (bytesRemaining >= 0) { + _remaining = bytesRemaining; + _total = bytesRemaining + alreadyTransferred; + } + _transferred = bytesTransferred; + _active = false; + } + + public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { + if (bytesRemaining >= 0) { + _remaining = bytesRemaining; + } + _transferred = bytesTransferred; + _active = false; + } + + public void headerReceived(String url, int attemptNum, String key, String val) {} + + public void attempting(String url) {} + + // End of EepGet status listeners + + private String _(String s) { + return _mgr.util().getString(s); + } + + private String _(String s, String o) { + return _mgr.util().getString(s, o); + } + + private static String urlify(String s) { + return I2PSnarkServlet.urlify(s); + } +} 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 640cc7a93..6ba8172be 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -540,11 +540,8 @@ public class I2PSnarkServlet extends DefaultServlet { *****/ if (newURL != null) { if (newURL.startsWith("http://")) { - if (!_manager.util().connected()) - _manager.addMessage(_("Opening the I2P tunnel")); - _manager.addMessage(_("Fetching {0}", urlify(newURL))); - I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add", true); - fetch.start(); + FetchAndAdd fetch = new FetchAndAdd(_context, _manager, newURL); + _manager.addDownloader(fetch); } else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) { addMagnet(newURL); } else if (newURL.length() == 40 && newURL.replaceAll("[a-fA-F0-9]", "").length() == 0) { @@ -880,7 +877,17 @@ public class I2PSnarkServlet extends DefaultServlet { */ private class TorrentNameComparator implements Comparator { private final Comparator collator = Collator.getInstance(); - private final String skip = _manager.getDataDir().getAbsolutePath() + File.separator; + private final String skip; + + public TorrentNameComparator() { + String s; + try { + s = _manager.getDataDir().getCanonicalPath(); + } catch (IOException ioe) { + s = _manager.getDataDir().getAbsolutePath(); + } + skip = s + File.separator; + } public int compare(String l, String r) { if (l.startsWith(skip)) @@ -916,8 +923,12 @@ public class I2PSnarkServlet extends DefaultServlet { private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException { String filename = snark.getName(); - File f = new File(filename); - filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name + if (snark.getMetaInfo() != null) { + // Only do this if not a magnet or torrent download + // Strip full path down to the local name + File f = new File(filename); + filename = f.getName(); + } int i = filename.lastIndexOf(".torrent"); if (i > 0) filename = filename.substring(0, i); @@ -1076,6 +1087,8 @@ public class I2PSnarkServlet extends DefaultServlet { icon = "folder"; else if (isValid) icon = toIcon(meta.getName()); + else if (snark instanceof FetchAndAdd) + icon = "arrow_down"; else icon = "magnet"; if (isValid) { @@ -1724,7 +1737,7 @@ public class I2PSnarkServlet extends DefaultServlet { } ihash = xt.substring("urn:btih:".length()); trackerURL = getTrackerParam(url); - name = "Magnet " + ihash; + name = "* " + _("Magnet") + ' ' + ihash; String dn = getParam("dn", url); if (dn != null) name += " (" + Storage.filterName(dn) + ')'; @@ -1734,7 +1747,7 @@ public class I2PSnarkServlet extends DefaultServlet { int col = ihash.indexOf(':'); if (col >= 0) ihash = ihash.substring(0, col); - name = "Maggot " + ihash; + name = "* " + _("Magnet") + ' ' + ihash; } else { return; } @@ -1941,7 +1954,7 @@ public class I2PSnarkServlet extends DefaultServlet { } /** @since 0.7.14 */ - private static String urlify(String s) { + static String urlify(String s) { return urlify(s, 100); } @@ -2357,83 +2370,4 @@ public class I2PSnarkServlet extends DefaultServlet { snark.updatePiecePriorities(); _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities()); } - - -/** inner class, don't bother reindenting */ -private static class FetchAndAdd implements Runnable { - private SnarkManager _manager; - private String _url; - public FetchAndAdd(SnarkManager mgr, String url) { - _manager = mgr; - _url = url; - } - public void run() { - _url = _url.trim(); - // 3 retries - File file = _manager.util().get(_url, false, 3); - try { - if ( (file != null) && (file.exists()) && (file.length() > 0) ) { - _manager.addMessage(_("Torrent fetched from {0}", urlify(_url))); - FileInputStream in = null; - try { - in = new FileInputStream(file); - byte[] fileInfoHash = new byte[20]; - String name = MetaInfo.getNameAndInfoHash(in, fileInfoHash); - try { in.close(); } catch (IOException ioe) {} - Snark snark = _manager.getTorrentByInfoHash(fileInfoHash); - if (snark != null) { - _manager.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName())); - return; - } - - name = Storage.filterName(name); - name = name + ".torrent"; - File torrentFile = new File(_manager.getDataDir(), name); - - String canonical = torrentFile.getCanonicalPath(); - - if (torrentFile.exists()) { - if (_manager.getTorrent(canonical) != null) - _manager.addMessage(_("Torrent already running: {0}", name)); - else - _manager.addMessage(_("Torrent already in the queue: {0}", name)); - } else { - // This may take a LONG time to create the storage. - _manager.copyAndAddTorrent(file, canonical); - } - } catch (IOException ioe) { - _manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage()); - } catch (OutOfMemoryError oom) { - _manager.addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + oom.getMessage()); - } finally { - try { if (in != null) in.close(); } catch (IOException ioe) {} - } - } else { - // Generate a retry link, but sadly can't have a form inside a table - // So make this an ugly GET - StringBuilder buf = new StringBuilder(1024); - // FIXME don't lose peer setting - //String peerParam = req.getParameter("p"); - //if (peerParam != null) - // buf.append("\n"); - buf.append(_("Torrent was not retrieved from {0}", urlify(_url))); - /**** FIXME ticket #575 - String link = urlEncode(_url).replace(":", "%3A").replace("/", "%2F"); - buf.append(" - ["); - buf.append(_("Retry")); - buf.append("]"); - ****/ - _manager.addMessage(buf.toString()); - } - } finally { - if (file != null) file.delete(); - } - } - - private String _(String s, String o) { - return _manager.util().getString(s, o); - } - -} - } diff --git a/history.txt b/history.txt index f0107f6f2..f1c8d2438 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,11 @@ +2012-06-11 zzz + * i2psnark: + - Display torrent file downloads in torrent area + - Sort magnets and downloads first + - Fix sorting problem when torrent dir is a symlink + - Reduce max file idle time + * NativeBigInteger: Workaround for Raspberry Pi to load the correct lib + 2012-06-08 zzz * i2psnark: - Move private tracker config from create box to torrent config diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 5a58b19bc..e36cd3b14 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 11; + public final static long BUILD = 12; /** for example "-test" */ public final static String EXTRA = "";