diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 4a09175b2..82ced2c28 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -656,10 +656,10 @@ public class Snark ioe.printStackTrace(); } savedUploaded = nowUploaded; - if (changed && completeListener != null) - completeListener.updateStatus(this); - // TODO should save comments at shutdown even if never started... + // SnarkManager.stopAllTorrents() will save comments at shutdown even if never started... if (completeListener != null) { + if (changed) + completeListener.updateStatus(this); synchronized(_commentLock) { if (_comments != null) { synchronized(_comments) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index d64ffceb5..541d559e9 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -116,6 +116,8 @@ public class SnarkManager implements CompleteListener, ClientApp { private static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet."; /** @since 0.9.31 */ private static final String PROP_META_COMMENTS = "comments"; + /** @since 0.9.42 */ + private static final String PROP_META_ACTIVITY = "activity"; private static final String CONFIG_FILE_SUFFIX = ".config"; private static final String CONFIG_FILE = "i2psnark" + CONFIG_FILE_SUFFIX; @@ -1609,13 +1611,16 @@ public class SnarkManager implements CompleteListener, ClientApp { } // ok, snark created, now lets start it up or configure it further Properties config = getConfig(torrent); - boolean running; - String prop = config.getProperty(PROP_META_RUNNING); - if(prop == null || Boolean.parseBoolean(prop)) { - running = true; - } else { - running = false; + String prop = config.getProperty(PROP_META_RUNNING); + boolean running = prop == null || Boolean.parseBoolean(prop); + prop = config.getProperty(PROP_META_ACTIVITY); + if (prop != null && torrent.getStorage() != null) { + try { + long activity = Long.parseLong(prop); + torrent.getStorage().setActivity(activity); + } catch (NumberFormatException nfe) {} } + // Were we running last time? String link = linkify(torrent); if (!dontAutoStart && shouldAutoStart() && running) { @@ -1779,7 +1784,7 @@ public class SnarkManager implements CompleteListener, ClientApp { addMessage(_t("Torrent with this info hash is already running: {0}", snark.getBaseName())); return false; } else if (bitfield != null) { - saveTorrentStatus(metainfo, bitfield, null, false, baseFile, true, 0, true); // no file priorities + saveTorrentStatus(metainfo, bitfield, null, false, baseFile, true, 0, 0, true); // no file priorities } // so addTorrent won't recheck if (filename == null) { @@ -2041,7 +2046,7 @@ public class SnarkManager implements CompleteListener, ClientApp { return; saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getInOrder(), storage.getBase(), storage.getPreserveFileNames(), - snark.getUploaded(), snark.isStopped(), comments); + snark.getUploaded(), storage.getActivity(), snark.isStopped(), comments); } /** @@ -2057,8 +2062,8 @@ public class SnarkManager implements CompleteListener, ClientApp { * @param base may be null */ private void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, boolean inOrder, - File base, boolean preserveNames, long uploaded, boolean stopped) { - saveTorrentStatus(metainfo, bitfield, priorities, inOrder, base, preserveNames, uploaded, stopped, null); + File base, boolean preserveNames, long uploaded, long activity, boolean stopped) { + saveTorrentStatus(metainfo, bitfield, priorities, inOrder, base, preserveNames, uploaded, activity, stopped, null); } /* @@ -2066,15 +2071,15 @@ public class SnarkManager implements CompleteListener, ClientApp { * @since 0.9.31 */ private void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, boolean inOrder, - File base, boolean preserveNames, long uploaded, boolean stopped, + File base, boolean preserveNames, long uploaded, long activity, boolean stopped, Boolean comments) { synchronized (_configLock) { - locked_saveTorrentStatus(metainfo, bitfield, priorities, inOrder, base, preserveNames, uploaded, stopped, comments); + locked_saveTorrentStatus(metainfo, bitfield, priorities, inOrder, base, preserveNames, uploaded, activity, stopped, comments); } } private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, boolean inOrder, - File base, boolean preserveNames, long uploaded, boolean stopped, + File base, boolean preserveNames, long uploaded, long activity, boolean stopped, Boolean comments) { byte[] ih = metainfo.getInfoHash(); Properties config = getConfig(ih); @@ -2104,6 +2109,8 @@ public class SnarkManager implements CompleteListener, ClientApp { config.setProperty(PROP_META_BASE, base.getAbsolutePath()); if (comments != null) config.setProperty(PROP_META_COMMENTS, comments.toString()); + if (activity > 0) + config.setProperty(PROP_META_ACTIVITY, Long.toString(activity)); // now the file priorities if (priorities != null) { @@ -2469,7 +2476,7 @@ public class SnarkManager implements CompleteListener, ClientApp { Storage storage = snark.getStorage(); if (meta != null && storage != null) saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getInOrder(), - storage.getBase(), storage.getPreserveFileNames(), snark.getUploaded(), + storage.getBase(), storage.getPreserveFileNames(), snark.getUploaded(), storage.getActivity(), snark.isStopped()); } @@ -2493,7 +2500,7 @@ public class SnarkManager implements CompleteListener, ClientApp { return null; } saveTorrentStatus(meta, storage.getBitField(), null, false, - storage.getBase(), storage.getPreserveFileNames(), 0, + storage.getBase(), storage.getPreserveFileNames(), 0, 0, snark.isStopped()); // temp for addMessage() in case canonical throws String name = storage.getBaseName(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 77843cd74..16d214f67 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -35,12 +35,14 @@ import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import gnu.getopt.Getopt; @@ -78,6 +80,7 @@ public class Storage implements Closeable private boolean _inOrder; private final AtomicInteger _allocateCount = new AtomicInteger(); private final AtomicInteger _checkProgress = new AtomicInteger(); + private final AtomicLong _activity = new AtomicLong(); /** The default piece size. */ private static final int DEFAULT_PIECE_SIZE = 256*1024; @@ -319,6 +322,28 @@ public class Storage implements Closeable changed = false; } + /** + * @since 0.9.42 + */ + public long getActivity() { + return _activity.get(); + } + + /** + * @since 0.9.42 + */ + private void setActivity() { + setActivity(I2PAppContext.getGlobalContext().clock().now()); + } + + /** + * @since 0.9.42 + */ + public void setActivity(long time) { + _activity.set(time); + changed = true; + } + /** * File checking in progress. * @since 0.9.3 @@ -827,6 +852,13 @@ public class Storage implements Closeable 0x2028, 0x2029 }; + // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file + private static final String[] WIN_ILLEGAL = new String[] { + "con", "prn", "aux", "nul", + "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", + "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" + }; + /** * Filter the name, but only if configured to do so. * We will do so on torrents received from others, but not @@ -859,8 +891,18 @@ public class Storage implements Closeable rv = "_"; } else { rv = name; - if (rv.startsWith(".")) + if (rv.startsWith(".")) { rv = '_' + rv.substring(1); + } else if (SystemVersion.isWindows()) { + // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file + String iname = name.toLowerCase(Locale.US); + for (int i = 0; i < WIN_ILLEGAL.length; i++) { + String w = WIN_ILLEGAL[i]; + if (iname.equals(w) || + (iname.startsWith(w + '.') && w.indexOf('.', w.length() + 1) < 0)) + rv = '_' + rv; + } + } if (rv.endsWith(".") || rv.endsWith(" ")) rv = rv.substring(0, rv.length() - 1) + '_'; for (int i = 0; i < ILLEGAL.length; i++) { @@ -1227,6 +1269,7 @@ public class Storage implements Closeable } bs = rv.getData(); getUncheckedPiece(piece, bs, off, len); + setActivity(); return rv; } @@ -1311,7 +1354,7 @@ public class Storage implements Closeable pp.release(); } - changed = true; + setActivity(); // do this after the write, so we know it succeeded, and we don't set the // needed count to zero, which would cause checkRAF() to open the file readonly. @@ -1567,8 +1610,7 @@ public class Storage implements Closeable * Caller must synchronize and call checkRAF() or openRAF(). * @since 0.9.1 */ - public synchronized void balloonFile() throws IOException - { + private synchronized void balloonFile() throws IOException { long remaining = length; final int ZEROBLOCKSIZE = (int) Math.min(remaining, 32*1024); byte[] zeros = new byte[ZEROBLOCKSIZE]; diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 435b6a04e..24be8f5b9 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -292,7 +292,7 @@ public class TrackerClient implements Runnable { _log.debug("Announce: [" + primary + "] infoHash: " + infoHash); } else { if (_log.shouldLog(Log.WARN)) - _log.warn("Skipping invalid or non-i2p announce: " + primary); + _log.warn("Skipping invalid or non-i2p announce: " + primary + " for torrent " + snark.getBaseName()); } } else { _log.warn("No primary announce"); @@ -366,7 +366,7 @@ public class TrackerClient implements Runnable { Hash h = getHostHash(ann); if (h == null) { if (_log.shouldLog(Log.WARN)) - _log.warn("Bad announce URL: [" + ann + ']'); + _log.warn("Bad announce URL: [" + ann + "] for torrent " + snark.getBaseName()); return false; } // comment this out if tracker.welterde.i2p upgrades @@ -374,19 +374,19 @@ public class TrackerClient implements Runnable { Destination dest = _util.getMyDestination(); if (dest != null && dest.getSigType() != SigType.DSA_SHA1) { if (_log.shouldLog(Log.WARN)) - _log.warn("Skipping incompatible tracker: " + ann); + _log.warn("Skipping incompatible tracker: " + ann + " for torrent " + snark.getBaseName()); return false; } } if (existing.size() >= MAX_TRACKERS) { if (_log.shouldLog(Log.INFO)) - _log.info("Not using announce URL, we have enough: [" + ann + ']'); + _log.info("Not using announce URL, we have enough: [" + ann + "] for torrent " + snark.getBaseName()); return false; } boolean rv = existing.add(h); if (!rv) { if (_log.shouldLog(Log.INFO)) - _log.info("Dup announce URL: [" + ann + ']'); + _log.info("Dup announce URL: [" + ann + "] for torrent " + snark.getBaseName()); } return rv; } @@ -605,7 +605,7 @@ public class TrackerClient implements Runnable { tplc.startsWith(ERROR_GOT_HTML) || // fake msg from doRequest() (!tr.isPrimary && tr.registerFails > MAX_REGISTER_FAILS / 2)) if (_log.shouldLog(Log.WARN)) - _log.warn("Not longer announcing to " + tr.announce + " : " + + _log.warn("No longer announcing to " + tr.announce + " : " + tr.trackerProblems + " after " + tr.registerFails + " failures"); tr.stop = true; // @@ -917,8 +917,21 @@ public class TrackerClient implements Runnable { if (!"http".equals(url.getScheme())) return null; String host = url.getHost(); - if (host == null) + if (host == null) { + // URI can't handle b64dest or b64dest.i2p if it contains '~' + // but it doesn't throw an exception, just returns a null host + if (ann.startsWith("http://") && ann.length() >= 7 + 516 && ann.contains("~")) { + ann = ann.substring(7); + int slash = ann.indexOf('/'); + if (slash >= 516) { + ann = ann.substring(0, slash); + if (ann.endsWith(".i2p")) + ann = ann.substring(0, ann.length() - 4); + return ConvertToHash.getHash(ann); + } + } return null; + } if (host.endsWith(".i2p")) { String path = url.getPath(); if (path == null || !path.startsWith("/")) 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 76c2e87ad..463d44618 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -3106,7 +3106,7 @@ public class I2PSnarkServlet extends BasicServlet { } long dat = meta.getCreationDate(); // needs locale configured for automatic translation - SimpleDateFormat fmt = new SimpleDateFormat("HH:mm, EEEE dd MMMM yyyy"); + SimpleDateFormat fmt = new SimpleDateFormat("EEEE dd MMMM yyyy HH:mm"); fmt.setTimeZone(SystemVersion.getSystemTimeZone(_context)); if (dat > 0) { String date = fmt.format(new Date(dat)); @@ -3147,6 +3147,18 @@ public class I2PSnarkServlet extends BasicServlet { .append(date) .append("\n"); } + if (storage != null) { + dat = storage.getActivity(); + if (dat > 0) { + String date = fmt.format(new Date(dat)); + buf.append(""); + toThemeImg(buf, "details"); + buf.append("") + .append(_t("Last activity")).append(": ") + .append(date) + .append("\n"); + } + } } if (meta == null || !meta.isPrivate()) { diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketEepGet.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketEepGet.java index 3be6a8c32..513a0545d 100644 --- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketEepGet.java +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketEepGet.java @@ -15,6 +15,7 @@ import net.i2p.I2PException; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.data.Base32; +import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; @@ -39,7 +40,8 @@ import net.i2p.util.SocketTimeout; * Supports http://example.i2p/blah * Supports http://B32KEY.b32.i2p/blah * Supports http://i2p/B64KEY/blah for compatibility with the eepproxy - * Supports http://B64KEY/blah for compatibility with the eepproxy + * Supports http://B64KEY/blah for as of 0.9.42 + * Supports http://B64KEY.i2p/blah as of 0.9.42 * Warning - does not support /eepproxy/blah, address helpers, http://B64KEY.i2p/blah, * or other odd things that may be found in the HTTP proxy. * @@ -115,9 +117,28 @@ public class I2PSocketEepGet extends EepGet { try { URI url = new URI(_actualURL); if ("http".equals(url.getScheme())) { + Destination dest = null; String host = url.getHost(); - if (host == null) - throw new MalformedURLException("no hostname: " + _actualURL); + if (host == null) { + String ann = _actualURL; + // URI can't handle b64dest.i2p if it contains '~' + // but it doesn't throw an exception, just returns a null host + if (ann.startsWith("http://") && ann.length() >= 7 + 516 && ann.contains("~")) { + ann = ann.substring(7); + int slash = ann.indexOf('/'); + if (slash >= 516) { + ann = ann.substring(0, slash); + if (ann.endsWith(".i2p")) + ann = ann.substring(0, ann.length() - 4); + try { + dest = new Destination(ann); + } catch (DataFormatException dfe) {} + } + } + if (dest == null) + throw new MalformedURLException("no hostname: " + _actualURL); + // won't pick up the port either, but the path will be OK + } int port = url.getPort(); if (port <= 0 || port > 65535) port = 80; @@ -139,31 +160,32 @@ public class I2PSocketEepGet extends EepGet { } } - // Use existing I2PSession for lookups. - // This is much more efficient than using the naming service - Destination dest; - I2PSession sess = _socketManager.getSession(); - if (sess != null && !sess.isClosed()) { - try { - if (host.length() == 60 && host.endsWith(".b32.i2p")) { - byte[] b = Base32.decode(host.substring(0, 52)); - if (b != null) { - Hash h = Hash.create(b); - dest = sess.lookupDest(h, 20*1000); + if (dest == null) { + // Use existing I2PSession for lookups. + // This is much more efficient than using the naming service + I2PSession sess = _socketManager.getSession(); + if (sess != null && !sess.isClosed()) { + try { + if (host.length() == 60 && host.endsWith(".b32.i2p")) { + byte[] b = Base32.decode(host.substring(0, 52)); + if (b != null) { + Hash h = Hash.create(b); + dest = sess.lookupDest(h, 20*1000); + } else { + dest = null; + } } else { - dest = null; + dest = sess.lookupDest(host, 20*1000); } - } else { - dest = sess.lookupDest(host, 20*1000); + } catch (I2PSessionException ise) { + dest = null; } - } catch (I2PSessionException ise) { - dest = null; + } else { + dest = _context.namingService().lookup(host); } - } else { - dest = _context.namingService().lookup(host); + if (dest == null) + throw new UnknownHostException("Unknown or non-i2p host: " + host); } - if (dest == null) - throw new UnknownHostException("Unknown or non-i2p host: " + host); // Set the timeouts, using the other existing options in the socket manager // This currently duplicates what SocketTimeout is doing in EepGet, @@ -257,7 +279,7 @@ public class I2PSocketEepGet extends EepGet { * Uses I2CP at localhost:7654 with a single 1-hop tunnel each direction. * Tunnel build time not included in the timeout. * - * This is just for testing, it will be commented out someday. + * This is just for testing. * Real command line apps should use EepGet.main(), * which has more options, and you don't have to wait for tunnels to be built. */