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("