merge of '398a24f487b61ef778a2e849660e953ef7e43b39'

and '598d00efae4c9b675b64fd626bc2eab2b921e0c5'
This commit is contained in:
zzz
2010-11-03 16:04:14 +00:00
42 changed files with 1005 additions and 231 deletions

View File

@ -152,6 +152,9 @@ public class I2PSnarkUtil {
*/
synchronized public boolean connect() {
if (_manager == null) {
// try to find why reconnecting after stop
if (_log.shouldLog(Log.DEBUG))
_log.debug("Connecting to I2P", new Exception("I did it"));
Properties opts = new Properties();
if (_opts != null) {
for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) {
@ -163,6 +166,10 @@ public class I2PSnarkUtil {
opts.setProperty("inbound.nickname", "I2PSnark");
if (opts.getProperty("outbound.nickname") == null)
opts.setProperty("outbound.nickname", "I2PSnark");
// Dont do this for now, it is set in I2PSocketEepGet for announces,
// we don't need fast handshake for peer connections.
//if (opts.getProperty("i2p.streaming.connectDelay") == null)
// opts.setProperty("i2p.streaming.connectDelay", "500");
if (opts.getProperty("i2p.streaming.inactivityTimeout") == null)
opts.setProperty("i2p.streaming.inactivityTimeout", "240000");
if (opts.getProperty("i2p.streaming.inactivityAction") == null)
@ -186,6 +193,7 @@ public class I2PSnarkUtil {
*/
public void disconnect() {
I2PSocketManager mgr = _manager;
// FIXME this can cause race NPEs elsewhere
_manager = null;
_shitlist.clear();
mgr.destroySocketManager();
@ -197,6 +205,9 @@ public class I2PSnarkUtil {
/** connect to the given destination */
I2PSocket connect(PeerID peer) throws IOException {
I2PSocketManager mgr = _manager;
if (mgr == null)
throw new IOException("No socket manager");
Destination addr = peer.getAddress();
if (addr == null)
throw new IOException("Null address");

View File

@ -56,8 +56,8 @@ public class Peer implements Comparable
private long _id;
final static long CHECK_PERIOD = PeerCoordinator.CHECK_PERIOD; // 40 seconds
final static int RATE_DEPTH = PeerCoordinator.RATE_DEPTH; // make following arrays RATE_DEPTH long
private long uploaded_old[] = {-1,-1,-1,-1,-1,-1};
private long downloaded_old[] = {-1,-1,-1,-1,-1,-1};
private long uploaded_old[] = {-1,-1,-1};
private long downloaded_old[] = {-1,-1,-1};
/**
* Creates a disconnected peer given a PeerID, your own id and the
@ -117,10 +117,15 @@ public class Peer implements Comparable
}
/**
* Returns socket (for debug printing)
* @return socket debug string (for debug printing)
*/
public String getSocket()
{
if (state != null) {
String r = state.getRequests();
if (r != null)
return sock.toString() + "<br>Requests: " + r;
}
return sock.toString();
}
@ -387,6 +392,37 @@ public class Peer implements Comparable
s.havePiece(piece);
}
/**
* Tell the other side that we are no longer interested in any of
* the outstanding requests (if any) for this piece.
* @since 0.8.1
*/
void cancel(int piece) {
PeerState s = state;
if (s != null)
s.cancelPiece(piece);
}
/**
* Are we currently requesting the piece?
* @since 0.8.1
*/
boolean isRequesting(int p) {
PeerState s = state;
return s != null && s.isRequesting(p);
}
/**
* Update the request queue.
* Call after adding wanted pieces.
* @since 0.8.1
*/
void request() {
PeerState s = state;
if (s != null)
s.addRequest();
}
/**
* Whether or not the peer is interested in pieces we have. Returns
* false if not connected.
@ -545,17 +581,8 @@ public class Peer implements Comparable
*/
public void setRateHistory(long up, long down)
{
setRate(up, uploaded_old);
setRate(down, downloaded_old);
}
private void setRate(long val, long array[])
{
synchronized(array) {
for (int i = RATE_DEPTH-1; i > 0; i--)
array[i] = array[i-1];
array[0] = val;
}
PeerCoordinator.setRate(up, uploaded_old);
PeerCoordinator.setRate(down, downloaded_old);
}
/**
@ -563,28 +590,11 @@ public class Peer implements Comparable
*/
public long getUploadRate()
{
return getRate(uploaded_old);
return PeerCoordinator.getRate(uploaded_old);
}
public long getDownloadRate()
{
return getRate(downloaded_old);
return PeerCoordinator.getRate(downloaded_old);
}
private long getRate(long array[])
{
long rate = 0;
int i = 0;
synchronized(array) {
for ( ; i < RATE_DEPTH; i++){
if (array[i] < 0)
break;
rate += array[i];
}
}
if (i == 0)
return 0;
return rate / (i * CHECK_PERIOD / 1000);
}
}

View File

@ -57,9 +57,9 @@ public class PeerCoordinator implements PeerListener
private long uploaded;
private long downloaded;
final static int RATE_DEPTH = 6; // make following arrays RATE_DEPTH long
private long uploaded_old[] = {-1,-1,-1,-1,-1,-1};
private long downloaded_old[] = {-1,-1,-1,-1,-1,-1};
final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long
private long uploaded_old[] = {-1,-1,-1};
private long downloaded_old[] = {-1,-1,-1};
// synchronize on this when changing peers or downloaders
final List<Peer> peers = new ArrayList();
@ -78,6 +78,7 @@ public class PeerCoordinator implements PeerListener
private final CoordinatorListener listener;
public I2PSnarkUtil _util;
private static final Random _random = I2PAppContext.getGlobalContext().random();
public String trackerProblems = null;
public int trackerSeenPeers = 0;
@ -97,20 +98,29 @@ public class PeerCoordinator implements PeerListener
// Install a timer to check the uploaders.
// Randomize the first start time so multiple tasks are spread out,
// this will help the behavior with global limits
Random r = I2PAppContext.getGlobalContext().random();
timer.schedule(new PeerCheckerTask(_util, this), (CHECK_PERIOD / 2) + r.nextInt((int) CHECK_PERIOD), CHECK_PERIOD);
timer.schedule(new PeerCheckerTask(_util, this), (CHECK_PERIOD / 2) + _random.nextInt((int) CHECK_PERIOD), CHECK_PERIOD);
}
// only called externally from Storage after the double-check fails
public void setWantedPieces()
{
// Make a list of pieces
// FIXME synchronize, clear and re-add instead?
// Don't replace something we are synchronizing on.
wantedPieces = new ArrayList();
BitField bitfield = storage.getBitField();
for(int i = 0; i < metainfo.getPieces(); i++)
if (!bitfield.get(i))
wantedPieces.add(new Piece(i));
Collections.shuffle(wantedPieces);
int[] pri = storage.getPiecePriorities();
for(int i = 0; i < metainfo.getPieces(); i++) {
// only add if we don't have and the priority is >= 0
if ((!bitfield.get(i)) &&
(pri == null || pri[i] >= 0)) {
Piece p = new Piece(i);
if (pri != null)
p.setPriority(pri[i]);
wantedPieces.add(p);
}
}
Collections.shuffle(wantedPieces, _random);
}
public Storage getStorage() { return storage; }
@ -183,7 +193,7 @@ public class PeerCoordinator implements PeerListener
setRate(down, downloaded_old);
}
private static void setRate(long val, long array[])
static void setRate(long val, long array[])
{
synchronized(array) {
for (int i = RATE_DEPTH-1; i > 0; i--)
@ -214,20 +224,23 @@ public class PeerCoordinator implements PeerListener
return (r * 1000) / CHECK_PERIOD;
}
private long getRate(long array[])
static long getRate(long array[])
{
long rate = 0;
int i = 0;
int factor = 0;
synchronized(array) {
for ( ; i < RATE_DEPTH; i++) {
if (array[i] < 0)
break;
rate += array[i];
int f = RATE_DEPTH - i;
rate += array[i] * f;
factor += f;
}
}
if (i == 0)
return 0;
return rate / (i * CHECK_PERIOD / 1000);
return rate / (factor * CHECK_PERIOD / 1000);
}
public MetaInfo getMetaInfo()
@ -454,7 +467,7 @@ public class PeerCoordinator implements PeerListener
}
/**
* Returns true if we don't have the given piece yet.
* @return true if we still want the given piece
*/
public boolean gotHave(Peer peer, int piece)
{
@ -499,6 +512,12 @@ public class PeerCoordinator implements PeerListener
*/
private static final int END_GAME_THRESHOLD = 8;
/**
* Max number of peers to get a piece from when in end game
* @since 0.8.1
*/
private static final int MAX_PARALLEL_REQUESTS = 4;
/**
* Returns one of pieces in the given BitField that is still wanted or
* -1 if none of the given pieces are wanted.
@ -520,6 +539,9 @@ public class PeerCoordinator implements PeerListener
while (piece == null && it.hasNext())
{
Piece p = it.next();
// sorted by priority, so when we hit a disabled piece we are done
if (p.isDisabled())
break;
if (havePieces.get(p.getId()) && !p.isRequested())
{
piece = p;
@ -538,15 +560,32 @@ public class PeerCoordinator implements PeerListener
if (wantedPieces.size() > END_GAME_THRESHOLD)
return -1; // nothing to request and not in end game
// let's not all get on the same piece
Collections.shuffle(requested);
// Even better would be to sort by number of requests
Collections.shuffle(requested, _random);
Iterator<Piece> it2 = requested.iterator();
while (piece == null && it2.hasNext())
{
Piece p = it2.next();
if (havePieces.get(p.getId()))
{
if (havePieces.get(p.getId())) {
// limit number of parallel requests
int requestedCount = 0;
synchronized(peers) {
for (Peer pr : peers) {
if (pr.isRequesting(p.getId())) {
if (pr.equals(peer)) {
// don't give it to him again
requestedCount = MAX_PARALLEL_REQUESTS;
break;
}
if (++requestedCount >= MAX_PARALLEL_REQUESTS)
break;
}
}
}
if (requestedCount >= MAX_PARALLEL_REQUESTS)
continue;
piece = p;
}
}
}
if (piece == null) {
if (_log.shouldLog(Log.WARN))
@ -555,7 +594,7 @@ public class PeerCoordinator implements PeerListener
// + " wanted = " + wantedPieces + " peerHas = " + havePieces);
return -1; //If we still can't find a piece we want, so be it.
} else {
// Should be a lot smarter here - limit # of parallel attempts and
// Should be a lot smarter here -
// share blocks rather than starting from 0 with each peer.
// This is where the flaws of the snark data model are really exposed.
// Could also randomize within the duplicate set rather than strict rarest-first
@ -563,11 +602,83 @@ public class PeerCoordinator implements PeerListener
_log.debug("parallel request (end game?) for " + peer + ": piece = " + piece);
}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Now requesting: piece " + piece + " priority " + piece.getPriority());
piece.setRequested(true);
return piece.getId();
}
}
/**
* Maps file priorities to piece priorities.
* Call after updating file priorities Storage.setPriority()
* @since 0.8.1
*/
public void updatePiecePriorities() {
int[] pri = storage.getPiecePriorities();
if (pri == null) {
_log.debug("Updated piece priorities called but no priorities to set?");
return;
}
synchronized(wantedPieces) {
// Add incomplete and previously unwanted pieces to the list
// Temp to avoid O(n**2)
BitField want = new BitField(pri.length);
for (Piece p : wantedPieces) {
want.set(p.getId());
}
BitField bitfield = storage.getBitField();
for (int i = 0; i < pri.length; i++) {
if (pri[i] >= 0 && !bitfield.get(i)) {
if (!want.get(i)) {
Piece piece = new Piece(i);
wantedPieces.add(piece);
// As connections are already up, new Pieces will
// not have their PeerID list populated, so do that.
synchronized(peers) {
for (Peer p : peers) {
PeerState s = p.state;
if (s != null) {
BitField bf = s.bitfield;
if (bf != null && bf.get(i))
piece.addPeer(p);
}
}
}
}
}
}
// now set the new priorities and remove newly unwanted pieces
for (Iterator<Piece> iter = wantedPieces.iterator(); iter.hasNext(); ) {
Piece p = iter.next();
int priority = pri[p.getId()];
if (priority >= 0) {
p.setPriority(priority);
} else {
iter.remove();
// cancel all peers
synchronized(peers) {
for (Peer peer : peers) {
peer.cancel(p.getId());
}
}
}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Updated piece priorities, now wanted: " + wantedPieces);
// if we added pieces, they will be in-order unless we shuffle
Collections.shuffle(wantedPieces, _random);
// update request queues, in case we added wanted pieces
// and we were previously uninterested
synchronized(peers) {
for (Peer peer : peers) {
peer.request();
}
}
}
}
/**
* Returns a byte array containing the requested piece or null of
* the piece is unknown.
@ -632,14 +743,18 @@ public class PeerCoordinator implements PeerListener
// No need to announce have piece to peers.
// Assume we got a good piece, we don't really care anymore.
return true;
// Well, this could be caused by a change in priorities, so
// only return true if we already have it, otherwise might as well keep it.
if (storage.getBitField().get(piece))
return true;
}
try
{
if (storage.putPiece(piece, bs))
{
_log.info("Got valid piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
if (_log.shouldLog(Log.INFO))
_log.info("Got valid piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
}
else
{

View File

@ -55,12 +55,10 @@ class PeerState
final PeerConnectionOut out;
// Outstanding request
private final List outstandingRequests = new ArrayList();
private final List<Request> outstandingRequests = new ArrayList();
/** the tail (NOT the head) of the request queue */
private Request lastRequest = null;
// If we have te resend outstanding requests (true after we got choked).
private boolean resend = false;
private final static int MAX_PIPELINE = 5; // this is for outbound requests
private final static int MAX_PIPELINE_BYTES = 128*1024; // this is for inbound requests
public final static int PARTSIZE = 16*1024; // outbound request
@ -91,14 +89,13 @@ class PeerState
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv " + (choke ? "" : "un") + "choked");
boolean resend = choked && !choke;
choked = choke;
if (choked)
resend = true;
listener.gotChoke(peer, choke);
if (!choked && interesting)
request();
if (interesting && !choked)
request(resend);
}
void interestedMessage(boolean interest)
@ -278,7 +275,7 @@ class PeerState
synchronized private int getFirstOutstandingRequest(int piece)
{
for (int i = 0; i < outstandingRequests.size(); i++)
if (((Request)outstandingRequests.get(i)).piece == piece)
if (outstandingRequests.get(i).piece == piece)
return i;
return -1;
}
@ -313,12 +310,12 @@ class PeerState
Request req;
synchronized(this)
{
req = (Request)outstandingRequests.get(r);
req = outstandingRequests.get(r);
while (req.piece == piece && req.off != begin
&& r < outstandingRequests.size() - 1)
{
r++;
req = (Request)outstandingRequests.get(r);
req = outstandingRequests.get(r);
}
// Something wrong?
@ -342,7 +339,7 @@ class PeerState
+ ", wanted for peer: " + peer);
for (int i = 0; i < r; i++)
{
Request dropReq = (Request)outstandingRequests.remove(0);
Request dropReq = outstandingRequests.remove(0);
outstandingRequests.add(dropReq);
if (!choked)
out.sendRequest(dropReq);
@ -366,11 +363,11 @@ class PeerState
{
Request req = null;
for (int i = 0; i < outstandingRequests.size(); i++) {
Request r1 = (Request)outstandingRequests.get(i);
Request r1 = outstandingRequests.get(i);
int j = getFirstOutstandingRequest(r1.piece);
if (j == -1)
continue;
Request r2 = (Request)outstandingRequests.get(j);
Request r2 = outstandingRequests.get(j);
if (r2.off > 0 && ((req == null) || (r2.off > req.off)))
req = r2;
}
@ -398,7 +395,7 @@ class PeerState
}
Request req = null;
for (int i = 0; i < size; i++) {
Request r1 = (Request)outstandingRequests.get(i);
Request r1 = outstandingRequests.get(i);
if (pc != r1.piece) {
pc = r1.piece;
arr[pos++] = pc;
@ -423,32 +420,19 @@ class PeerState
+ " length: " + bs.length);
}
/**
* We now have this piece.
* Tell the peer and cancel any requests for the piece.
*/
void havePiece(int piece)
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("Tell " + peer + " havePiece(" + piece + ")");
synchronized(this)
{
// Tell the other side that we are no longer interested in any of
// the outstanding requests for this piece.
if (lastRequest != null && lastRequest.piece == piece)
lastRequest = null;
Iterator it = outstandingRequests.iterator();
while (it.hasNext())
{
Request req = (Request)it.next();
if (req.piece == piece)
{
it.remove();
// Send cancel even when we are choked to make sure that it is
// really never ever send.
out.sendCancel(req);
}
}
}
cancelPiece(piece);
// Tell the other side that we really have this piece.
out.sendHave(piece);
@ -463,8 +447,46 @@ class PeerState
}
}
// Starts or resumes requesting pieces.
private void request()
/**
* Tell the other side that we are no longer interested in any of
* the outstanding requests (if any) for this piece.
* @since 0.8.1
*/
synchronized void cancelPiece(int piece) {
if (lastRequest != null && lastRequest.piece == piece)
lastRequest = null;
Iterator<Request> it = outstandingRequests.iterator();
while (it.hasNext())
{
Request req = it.next();
if (req.piece == piece)
{
it.remove();
// Send cancel even when we are choked to make sure that it is
// really never ever send.
out.sendCancel(req);
}
}
}
/**
* Are we currently requesting the piece?
* @since 0.8.1
*/
synchronized boolean isRequesting(int piece) {
for (Request req : outstandingRequests) {
if (req.piece == piece)
return true;
}
return false;
}
/**
* Starts or resumes requesting pieces.
* @param resend should we resend outstanding requests?
*/
private void request(boolean resend)
{
// Are there outstanding requests that have to be resend?
if (resend)
@ -472,7 +494,6 @@ class PeerState
synchronized (this) {
out.sendRequests(outstandingRequests);
}
resend = false;
}
// Add/Send some more requests if necessary.
@ -481,8 +502,11 @@ class PeerState
/**
* Adds a new request to the outstanding requests list.
* Then send interested if we weren't.
* Then send new requests if not choked.
* If nothing to request, send not interested if we were.
*/
synchronized private void addRequest()
synchronized void addRequest()
{
boolean more_pieces = true;
while (more_pieces)
@ -526,6 +550,7 @@ class PeerState
/**
* Starts requesting first chunk of next piece. Returns true if
* something has been added to the requests, false otherwise.
* Caller should synchronize.
*/
private boolean requestNextPiece()
{
@ -553,11 +578,10 @@ class PeerState
}
}
int nextPiece = listener.wantPiece(peer, bitfield);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " want piece " + nextPiece);
if (nextPiece != -1
&& (lastRequest == null || lastRequest.piece != nextPiece))
{
if (nextPiece != -1
&& (lastRequest == null || lastRequest.piece != nextPiece)) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " want piece " + nextPiece);
// Fail safe to make sure we are interested
// When we transition into the end game we may not be interested...
if (!interesting) {
@ -584,9 +608,25 @@ class PeerState
out.sendRequest(req);
lastRequest = req;
return true;
}
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " no more pieces to request");
}
}
// failsafe
if (outstandingRequests.isEmpty())
lastRequest = null;
// If we are not in the end game, we may run out of things to request
// because we are asking other peers. Set not-interesting now rather than
// wait for those other requests to be satisfied via havePiece()
if (interesting && lastRequest == null) {
interesting = false;
out.sendInterest(false);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " nothing more to request, now uninteresting");
}
return false;
}
@ -601,7 +641,7 @@ class PeerState
out.sendInterest(interest);
if (interesting && !choked)
request();
request(true); // we shouldnt have any pending requests, but if we do, resend them
}
}
@ -627,4 +667,16 @@ class PeerState
if (interesting && !choked)
out.retransmitRequests(outstandingRequests);
}
/**
* debug
* @return string or null
* @since 0.8.1
*/
synchronized String getRequests() {
if (outstandingRequests.isEmpty())
return null;
else
return outstandingRequests.toString();
}
}

View File

@ -1,22 +1,31 @@
package org.klomp.snark;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import net.i2p.util.ConcurrentHashSet;
public class Piece implements Comparable {
private int id;
private Set peers;
private Set<PeerID> peers;
private boolean requested;
/** @since 0.8.1 */
private int priority;
public Piece(int id) {
this.id = id;
this.peers = Collections.synchronizedSet(new HashSet());
this.requested = false;
this.peers = new ConcurrentHashSet();
}
/**
* Highest priority first,
* then rarest first
*/
public int compareTo(Object o) throws ClassCastException {
int pdiff = ((Piece)o).priority - this.priority; // reverse
if (pdiff != 0)
return pdiff;
return this.peers.size() - ((Piece)o).peers.size();
}
@ -37,12 +46,25 @@ public class Piece implements Comparable {
}
public int getId() { return this.id; }
public Set getPeers() { return this.peers; }
/** @deprecated unused */
public Set<PeerID> getPeers() { return this.peers; }
public boolean addPeer(Peer peer) { return this.peers.add(peer.getPeerID()); }
public boolean removePeer(Peer peer) { return this.peers.remove(peer.getPeerID()); }
public boolean isRequested() { return this.requested; }
public void setRequested(boolean requested) { this.requested = requested; }
/** @return default 0 @since 0.8.1 */
public int getPriority() { return this.priority; }
/** @since 0.8.1 */
public void setPriority(int p) { this.priority = p; }
/** @since 0.8.1 */
public boolean isDisabled() { return this.priority < 0; }
/** @since 0.8.1 */
public void setDisabled() { this.priority = -1; }
@Override
public String toString() {
return String.valueOf(id);

View File

@ -54,6 +54,7 @@ public class SnarkManager implements Snark.CompleteListener {
public static final String PROP_DIR = "i2psnark.dir";
public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
public static final String PROP_META_PRIORITY_SUFFIX = ".priority";
private static final String CONFIG_FILE = "i2psnark.config";
public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops
@ -510,6 +511,7 @@ public class SnarkManager implements Snark.CompleteListener {
torrent = new Snark(_util, filename, null, -1, null, null, this,
_peerCoordinatorSet, _connectionAcceptor,
false, dataDir.getPath());
loadSavedFilePriorities(torrent);
torrent.completeListener = this;
synchronized (_snarks) {
_snarks.put(filename, torrent);
@ -587,6 +589,33 @@ public class SnarkManager implements Snark.CompleteListener {
return new BitField(bitfield, len);
}
/**
* Get the saved priorities for a torrent from the config file.
* @since 0.8.1
*/
public void loadSavedFilePriorities(Snark snark) {
MetaInfo metainfo = snark.meta;
if (metainfo.getFiles() == null)
return;
byte[] ih = metainfo.getInfoHash();
String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$');
String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
if (pri == null)
return;
int filecount = metainfo.getFiles().size();
int[] rv = new int[filecount];
String[] arr = pri.split(",");
for (int i = 0; i < filecount && i < arr.length; i++) {
if (arr[i].length() > 0) {
try {
rv[i] = Integer.parseInt(arr[i]);
} catch (Throwable t) {}
}
}
snark.storage.setFilePriorities(rv);
}
/**
* Save the completion status of a torrent and the current time in the config file
* in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield".
@ -595,8 +624,9 @@ public class SnarkManager implements Snark.CompleteListener {
* The time is a standard long converted to string.
* The status is either a bitfield converted to Base64 or "." for a completed
* torrent to save space in the config file and in memory.
* @param priorities may be null
*/
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) {
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
byte[] ih = metainfo.getInfoHash();
String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$');
@ -609,6 +639,34 @@ public class SnarkManager implements Snark.CompleteListener {
bfs = Base64.encode(bf);
}
_config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs);
// now the file priorities
String prop = PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX;
if (priorities != null) {
boolean nonzero = false;
for (int i = 0; i < priorities.length; i++) {
if (priorities[i] != 0) {
nonzero = true;
break;
}
}
if (nonzero) {
// generate string like -5,,4,3,,,,,,-2 where no number is zero.
StringBuilder buf = new StringBuilder(2 * priorities.length);
for (int i = 0; i < priorities.length; i++) {
if (priorities[i] != 0)
buf.append(Integer.toString(priorities[i]));
if (i != priorities.length - 1)
buf.append(',');
}
_config.setProperty(prop, buf.toString());
} else {
_config.remove(prop);
}
} else {
_config.remove(prop);
}
saveConfig();
}
@ -621,6 +679,7 @@ public class SnarkManager implements Snark.CompleteListener {
String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$');
_config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
_config.remove(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
saveConfig();
}
@ -742,7 +801,7 @@ public class SnarkManager implements Snark.CompleteListener {
}
public void updateStatus(Snark snark) {
saveTorrentStatus(snark.meta, snark.storage.getBitField());
saveTorrentStatus(snark.meta, snark.storage.getBitField(), snark.storage.getFilePriorities());
}
private void monitorTorrents(File dir) {

View File

@ -42,6 +42,8 @@ public class Storage
private Object[] RAFlock; // lock on RAF access
private long[] RAFtime; // when was RAF last accessed, or 0 if closed
private File[] RAFfile; // File to make it easier to reopen
/** priorities by file; default 0; may be null. @since 0.8.1 */
private int[] priorities;
private final StorageListener listener;
private I2PSnarkUtil _util;
@ -228,6 +230,8 @@ public class Storage
RAFlock = new Object[size];
RAFtime = new long[size];
RAFfile = new File[size];
priorities = new int[size];
int i = 0;
Iterator it = files.iterator();
@ -330,6 +334,102 @@ public class Storage
return -1;
}
/**
* @param file canonical path (non-directory)
* @since 0.8.1
*/
public int getPriority(String file) {
if (complete() || metainfo.getFiles() == null || priorities == null)
return 0;
for (int i = 0; i < rafs.length; i++) {
File f = RAFfile[i];
// use canonical in case snark dir or sub dirs are symlinked
if (f != null) {
try {
String canonical = f.getCanonicalPath();
if (canonical.equals(file))
return priorities[i];
} catch (IOException ioe) {}
}
}
return 0;
}
/**
* Must call setPiecePriorities() after calling this
* @param file canonical path (non-directory)
* @param priority default 0; <0 to disable
* @since 0.8.1
*/
public void setPriority(String file, int pri) {
if (complete() || metainfo.getFiles() == null || priorities == null)
return;
for (int i = 0; i < rafs.length; i++) {
File f = RAFfile[i];
// use canonical in case snark dir or sub dirs are symlinked
if (f != null) {
try {
String canonical = f.getCanonicalPath();
if (canonical.equals(file)) {
priorities[i] = pri;
return;
}
} catch (IOException ioe) {}
}
}
}
/**
* Get the file priorities array.
* @return null on error, if complete, or if only one file
* @since 0.8.1
*/
public int[] getFilePriorities() {
return priorities;
}
/**
* Set the file priorities array.
* Only call this when stopped, but after check()
* @param p may be null
* @since 0.8.1
*/
void setFilePriorities(int[] p) {
priorities = p;
}
/**
* Call setPriority() for all changed files first,
* then call this.
* Set the piece priority to the highest priority
* of all files spanning the piece.
* Caller must pass array to the PeerCoordinator.
* @return null on error, if complete, or if only one file
* @since 0.8.1
*/
public int[] getPiecePriorities() {
if (complete() || metainfo.getFiles() == null || priorities == null)
return null;
int[] rv = new int[metainfo.getPieces()];
int file = 0;
long pcEnd = -1;
long fileEnd = lengths[0] - 1;
int psz = metainfo.getPieceLength(0);
for (int i = 0; i < rv.length; i++) {
pcEnd += psz;
int pri = priorities[file];
while (fileEnd <= pcEnd && file < lengths.length - 1) {
file++;
long oldFileEnd = fileEnd;
fileEnd += lengths[file];
if (priorities[file] > pri && oldFileEnd < pcEnd)
pri = priorities[file];
}
rv[i] = pri;
}
return rv;
}
/**
* The BitField that tells which pieces this storage contains.
* Do not change this since this is the current state of the storage.
@ -436,10 +536,14 @@ public class Storage
changed = true;
checkCreateFiles();
}
if (complete())
if (complete()) {
_util.debug("Torrent is complete", Snark.NOTICE);
else
} else {
// fixme saved priorities
if (files != null)
priorities = new int[files.size()];
_util.debug("Still need " + needed + " out of " + metainfo.getPieces() + " pieces", Snark.NOTICE);
}
}
/**
@ -565,6 +669,10 @@ public class Storage
changed = true;
synchronized(RAFlock[i]) {
allocateFile(i);
// close as we go so we don't run out of file descriptors
try {
closeRAF(i);
} catch (IOException ioe) {}
}
} else {
_util.debug("File '" + names[i] + "' exists, but has wrong length - repairing corruption", Snark.ERROR);
@ -573,8 +681,10 @@ public class Storage
synchronized(RAFlock[i]) {
checkRAF(i);
rafs[i].setLength(lengths[i]);
try {
closeRAF(i);
} catch (IOException ioe) {}
}
// will be closed below
}
}
@ -583,10 +693,25 @@ public class Storage
{
pieces = metainfo.getPieces();
byte[] piece = new byte[metainfo.getPieceLength(0)];
int file = 0;
long fileEnd = lengths[0];
long pieceEnd = 0;
for (int i = 0; i < pieces; i++)
{
int length = getUncheckedPiece(i, piece);
boolean correctHash = metainfo.checkPiece(i, piece, 0, length);
// close as we go so we don't run out of file descriptors
pieceEnd += length;
while (fileEnd <= pieceEnd) {
synchronized(RAFlock[file]) {
try {
closeRAF(file);
} catch (IOException ioe) {}
}
if (++file >= rafs.length)
break;
fileEnd += lengths[file];
}
if (correctHash)
{
bitfield.set(i);
@ -601,13 +726,14 @@ public class Storage
_probablyComplete = complete();
// close all the files so we don't end up with a zillion open ones;
// we will reopen as needed
for (int i = 0; i < rafs.length; i++) {
synchronized(RAFlock[i]) {
try {
closeRAF(i);
} catch (IOException ioe) {}
}
}
// Now closed above to avoid running out of file descriptors
//for (int i = 0; i < rafs.length; i++) {
// synchronized(RAFlock[i]) {
// try {
// closeRAF(i);
// } catch (IOException ioe) {}
// }
//}
if (listener != null) {
listener.storageAllChecked(this);
@ -616,6 +742,7 @@ public class Storage
}
}
/** this calls openRAF(); caller must synnchronize and call closeRAF() */
private void allocateFile(int nr) throws IOException
{
// caller synchronized
@ -624,7 +751,12 @@ public class Storage
// the whole file?
listener.storageCreateFile(this, names[nr], lengths[nr]);
final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
byte[] zeros = new byte[ZEROBLOCKSIZE];
byte[] zeros;
try {
zeros = new byte[ZEROBLOCKSIZE];
} catch (OutOfMemoryError oom) {
throw new IOException(oom.toString());
}
int i;
for (i = 0; i < lengths[nr]/ZEROBLOCKSIZE; i++)
{

View File

@ -266,9 +266,9 @@ public class TrackerClient extends I2PAppThread
// we only want to talk to new people if we need things
// from them (duh)
List ordered = new ArrayList(peers);
Collections.shuffle(ordered);
Collections.shuffle(ordered, r);
Iterator it = ordered.iterator();
while (it.hasNext()) {
while ((!stop) && it.hasNext()) {
Peer cur = (Peer)it.next();
// FIXME if id == us || dest == us continue;
// only delay if we actually make an attempt to add peer
@ -357,7 +357,7 @@ public class TrackerClient extends I2PAppThread
+ "&uploaded=" + uploaded
+ "&downloaded=" + downloaded
+ "&left=" + left
+ "&compact"
+ "&compact=1" // NOTE: opentracker will return 400 for &compact alone
+ ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
s += "&numwant=0";

View File

@ -133,6 +133,7 @@ public class I2PSnarkServlet extends Default {
// bypass the horrid Resource.getListHTML()
String pathInfo = req.getPathInfo();
String pathInContext = URI.addPaths(path, pathInfo);
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8");
Resource resource = getResource(pathInContext);
@ -140,7 +141,7 @@ public class I2PSnarkServlet extends Default {
resp.sendError(HttpResponse.__404_Not_Found);
} else {
String base = URI.addPaths(req.getRequestURI(), "/");
String listing = getListHTML(resource, base, true);
String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null);
if (listing != null)
resp.getWriter().write(listing);
else // shouldn't happen
@ -241,7 +242,7 @@ public class I2PSnarkServlet extends Default {
out.write(TABLE_HEADER);
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/status.png\"");
out.write(" title=\"");
out.write(_("Torrent Status"));
out.write(_("Status"));
out.write("\">");
out.write(_("Status"));
if (_manager.util().connected() && !snarks.isEmpty()) {
@ -249,24 +250,24 @@ public class I2PSnarkServlet extends Default {
out.write(req.getRequestURI());
if (peerParam != null) {
out.write("\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/showpeers.png\" title=\"");
out.write(_("Toggle Peer Visibility"));
out.write("\" alt=\"");
out.write(_("Hide Peers"));
out.write("\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/showpeers.png\" title=\"");
out.write(_("Hide Peers"));
out.write("\" alt=\"");
out.write(_("Hide Peers"));
out.write("\">");
} else {
out.write("?p=1\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/hidepeers.png\" title=\"");
out.write(_("Toggle Peer Visibility"));
out.write("\" alt=\"");
out.write(_("Show Peers"));
out.write("\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/hidepeers.png\" title=\"");
out.write(_("Show Peers"));
out.write("\" alt=\"");
out.write(_("Show Peers"));
out.write("\">");
}
out.write("</a><br>\n");
}
out.write("</th>\n<th align=\"left\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/torrent.png\" title=\"");
out.write(_("Loaded Torrents"));
out.write(_("Torrent"));
out.write("\">");
out.write(_("Torrent"));
out.write("</th>\n<th align=\"center\">");
@ -276,21 +277,21 @@ public class I2PSnarkServlet extends Default {
out.write(_("ETA"));
out.write("</th>\n<th align=\"center\">");
out.write("<img border=\"0\" src=\"/themes/console/images/inbound.png\" title=\"");
out.write(_("Data Downloaded"));
out.write(_("Downloaded"));
out.write("\">");
out.write(_("RX"));
out.write("</th>\n<th align=\"center\">");
out.write("<img border=\"0\" src=\"/themes/console/images/outbound.png\" title=\"");
out.write(_("Data Uploaded"));
out.write(_("Uploaded"));
out.write("\">");
out.write(_("TX"));
out.write("</th>\n<th align=\"center\">");
out.write("<img border=\"0\" src=\"/themes/console/images/inbound.png\" title=\"");
out.write(_("Download Speed"));
out.write(_("Down Rate"));
out.write("\">Rate");
out.write("</th>\n<th align=\"center\">");
out.write("<img border=\"0\" src=\"/themes/console/images/outbound.png\" title=\"");
out.write(_("Upload Speed"));
out.write(_("Up Rate"));
out.write("\">");
out.write(_("Rate"));
out.write("</th>\n");
@ -301,7 +302,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Stop all torrents and the I2P tunnel"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/stop_all.png\" title=\"");
out.write(_("Stop All Torrents"));
out.write(_("Stop all torrents and the I2P tunnel"));
out.write("\" alt=\"");
out.write(_("Stop All"));
out.write("\">");
@ -312,7 +313,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Start all torrents and the I2P tunnel"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/start_all.png\" title=\"");
out.write(_("Start All Torrents"));
out.write(_("Start all torrents and the I2P tunnel"));
out.write("\" alt=\"Start All\">");
out.write("</a>");
} else {
@ -537,7 +538,7 @@ public class I2PSnarkServlet extends Default {
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
if (torrentFile.exists())
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
_manager.saveTorrentStatus(info, s.getBitField()); // so addTorrent won't recheck
_manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck
// DirMonitor could grab this first, maybe hold _snarks lock?
FileOutputStream out = new FileOutputStream(torrentFile);
out.write(info.getTorrentData());
@ -564,6 +565,8 @@ public class I2PSnarkServlet extends Default {
_manager.stopTorrent(snark.torrent, false);
}
if (_manager.util().connected()) {
// Give the stopped announces time to get out
try { Thread.sleep(2000); } catch (InterruptedException ie) {}
_manager.util().disconnect();
_manager.addMessage(_("I2P tunnel closed."));
}
@ -718,7 +721,7 @@ public class I2PSnarkServlet extends Default {
curPeers + "/" +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else
statusString = "<img border=\"0\" src=\"/themes/snark/ubergine/images/complete.png\" title=\"" + _("Complete") + "\">" + _("Not Seeding");
statusString = "<img border=\"0\" src=\"/themes/snark/ubergine/images/complete.png\" title=\"" + _("Complete") + "\">" + _("Complete");
} else {
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
statusString = "<img border=\"0\" src=\"/themes/snark/ubergine/images/downloading.png\" title=\"" + _("Downloading") + "\">" +
@ -727,7 +730,7 @@ public class I2PSnarkServlet extends Default {
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning && curPeers > 0 && downBps > 0)
statusString = "<img border=\"0\" src=\"/themes/snark/ubergine/images/downloading.png\" title=\"" + _("Downloading") + "\">" +
" (" + curPeers + "/" +
curPeers + "/" +
ngettext("1 peer", "{0} peers", knownPeers);
else if (isRunning && curPeers > 0 && !showPeers)
statusString = "<img border=\"0\" src=\"/themes/snark/ubergine/images/stalled.png\" title=\"" + _("Stalled") + "\">" +
@ -790,7 +793,7 @@ public class I2PSnarkServlet extends Default {
baseURL = baseURL.substring(e + 1);
out.write("&nbsp;<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash=");
out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
out.write("\" title=\"" + name + _("Tracker") + "\" target=\"_blank\">");
out.write("\" title=\"" + name + ' ' + _("Tracker") + "\" target=\"_blank\">");
out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/details.png\">");
out.write("</a>");
break;
@ -804,7 +807,7 @@ public class I2PSnarkServlet extends Default {
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
if (remaining > 0)
out.write(formatSize(total-remaining) + "/" + formatSize(total)); // 18MB/3GB
out.write(formatSize(total-remaining) + "&thinsp;/&thinsp;" + formatSize(total)); // 18MB/3GB; thin space so it will line break well
else
out.write(formatSize(total)); // 3GB
out.write("</td>\n\t");
@ -828,7 +831,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Stop the torrent"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/stop.png\" title=\"");
out.write(_("Stop Torrent"));
out.write(_("Stop"));
out.write("\" alt=\"");
out.write(_("Stop"));
out.write("\">");
@ -840,7 +843,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Start the torrent"));
out.write("\">");
out.write("<img src=\"/themes/snark/ubergine/images/start.png\" title=\"");
out.write(_("Start Torrent"));
out.write(_("Start the torrent"));
out.write("\" alt=\"");
out.write(_("Start"));
out.write("\">");
@ -856,7 +859,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
out.write("')) { return false; }\">");
out.write("<img src=\"/themes/snark/ubergine/images/remove.png\" title=\"");
out.write(_("Remove Torrent"));
out.write(_("Remove"));
out.write("\" alt=\"");
out.write(_("Remove"));
out.write("\">");
@ -871,7 +874,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
out.write("')) { return false; }\">");
out.write("<img src=\"/themes/snark/ubergine/images/delete.png\" title=\"");
out.write(_("Delete Torrent + Data"));
out.write(_("Delete"));
out.write("\" alt=\"");
out.write(_("Delete"));
out.write("\">");
@ -1007,7 +1010,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("Add torrent"));
out.write("\" name=\"foo\" ><br>\n");
out.write("<tr><td>&nbsp;<td><span class=\"snarkAddInfo\">");
out.write(_("You can also copy .torrent files to: {0}.", "<code>" + _manager.getDataDir().getAbsolutePath ())) + "</code>";
out.write(_("You can also copy .torrent files to: {0}.", "<code>" + _manager.getDataDir().getAbsolutePath () + "</code>"));
out.write("\n");
out.write(_("Removing a .torrent will cause it to stop."));
out.write("<br></span></table>\n");
@ -1262,19 +1265,21 @@ public class I2PSnarkServlet extends Default {
// rounding makes us look faster :)
private static String formatSize(long bytes) {
if (bytes < 5*1024)
return bytes + " B";
return bytes + "&nbsp;B";
else if (bytes < 5*1024*1024)
return ((bytes + 512)/1024) + " KB";
return ((bytes + 512)/1024) + "&nbsp;KB";
else if (bytes < 10*1024*1024*1024l)
return ((bytes + 512*1024)/(1024*1024)) + " MB";
return ((bytes + 512*1024)/(1024*1024)) + "&nbsp;MB";
else
return ((bytes + 512*1024*1024)/(1024*1024*1024)) + " GB";
return ((bytes + 512*1024*1024)/(1024*1024*1024)) + "&nbsp;GB";
}
/** @since 0.7.14 */
private static String urlify(String s) {
StringBuilder buf = new StringBuilder(256);
buf.append("<a href=\"").append(s).append("\">").append(s).append("</a>");
// browsers seem to work without doing this but let's be strict
String link = s.replace("&", "&amp;");
buf.append("<a href=\"").append(link).append("\">").append(link).append("</a>");
return buf.toString();
}
@ -1313,10 +1318,11 @@ public class I2PSnarkServlet extends Default {
* @param r The Resource
* @param base The base URL
* @param parent True if the parent directory should be included
* @param postParams map of POST parameters or null if not a POST
* @return String of HTML
* @since 0.7.14
*/
private String getListHTML(Resource r, String base, boolean parent)
private String getListHTML(Resource r, String base, boolean parent, Map postParams)
throws IOException
{
if (!r.isDirectory())
@ -1341,19 +1347,40 @@ public class I2PSnarkServlet extends Default {
else
torrentName = title;
Snark snark = _manager.getTorrentByBaseName(torrentName);
if (snark != null && postParams != null)
savePriorities(snark, postParams);
if (title.endsWith("/"))
title = title.substring(0, title.length() - 1);
title = _("Torrent") + ": " + title;
buf.append(title);
buf.append("</TITLE>").append(HEADER).append("<link rel=\"shortcut icon\" href=\"/themes/snark/ubergine/favicon.ico\"></HEAD><BODY>\n<center><div class=\"snarknavbar\"> <a href=\"/i2psnark/\" title=\"Torrents\"");
buf.append(" class=\"snarkRefresh\">I2PSnark</a>").append("</div>");
buf.append("<div class=\"page\"><div class=\"mainsection\">" +
"<TABLE BORDER=0 class=\"snarkTorrents\" cellpadding=\"5px 10px\">" +
if (parent)
{
buf.append("\n<br><A HREF=\"");
// corrupts utf-8
//buf.append(URI.encodePath(URI.addPaths(base,"../")));
buf.append(URI.addPaths(base,"../"));
buf.append("\"><img border=\"0\" src=\"/themes/console/images/outbound.png\"> ")
.append(_("Up to higher level directory")).append("</A>\n");
}
buf.append("</div><div class=\"page\"><div class=\"mainsection\">");
boolean showPriority = snark != null && !snark.storage.complete();
if (showPriority)
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" cellpadding=\"5px 10px\">" +
"<thead><tr><th>").append("<img border=\"0\" src=\"/themes/snark/ubergine/images/file.png\" title=\"").append(_("File")).append("\" alt=\"").append(_("File")).append("\">&nbsp;").append(title).append("</th><th align=\"right\">").append("<img border=\"0\" src=\"/themes/snark/ubergine/images/size.png\" title=\"").append(_("FileSize")).append("\" alt=\"").append(_("FileSize")).append("\">").append(_("Size"));
buf.append("</th><th>").append("<img border=\"0\" src=\"/themes/snark/ubergine/images/status.png\" title=\"").append(_("Download Status")).append("\">").append(_("Status")).append("</th></tr></thead>");
buf.append("</th><th>").append("<img border=\"0\" src=\"/themes/snark/ubergine/images/status.png\" title=\"").append(_("Download Status")).append("\">").append(_("Status")).append("</th>");
if (showPriority)
buf.append("<th>").append(_("Priority")).append("</th>");
buf.append("</tr></thead>\n");
//DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
// DateFormat.MEDIUM);
boolean showSaveButton = false;
for (int i=0 ; i< ls.length ; i++)
{
String encoded=URI.encodePath(ls[i]);
@ -1391,7 +1418,8 @@ public class I2PSnarkServlet extends Default {
complete = true;
status = toImg("tick") + _("Complete");
} else {
status = toImg("clock") +
status =
(snark.storage.getPriority(f.getCanonicalPath()) < 0 ? toImg("cancel") : toImg("clock")) +
(100 * (length - remaining) / length) + "% " + _("complete") +
" (" + DataHelper.formatSize2(remaining) + _("bytes remaining") + ")";
}
@ -1435,22 +1463,41 @@ public class I2PSnarkServlet extends Default {
buf.append("</TD><TD class=\"").append(rowClass).append(" snarkFileStatus\">");
//buf.append(dfmt.format(new Date(item.lastModified())));
buf.append(status);
buf.append("</TD></TR>\n");
buf.append("</TD>");
if (showPriority) {
buf.append("<td>");
File f = item.getFile();
if ((!complete) && (!item.isDirectory()) && f != null) {
int pri = snark.storage.getPriority(f.getCanonicalPath());
buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri > 0)
buf.append("checked=\"true\"");
buf.append('>').append(_("High"));
buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri == 0)
buf.append("checked=\"true\"");
buf.append('>').append(_("Normal"));
buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri < 0)
buf.append("checked=\"true\"");
buf.append('>').append(_("Do not download"));
showSaveButton = true;
}
buf.append("</td>");
}
buf.append("</TR>\n");
}
if (parent)
{
buf.append("<tfoot align=\"left\"><tr><td colspan=\"3\"><A HREF=\"");
// corrupts utf-8
//buf.append(URI.encodePath(URI.addPaths(base,"../")));
buf.append(URI.addPaths(base,"../"));
buf.append("\"><img border=\"0\" src=\"/themes/snark/ubergine/images/up.png\"> ")
.append(_("Up to higher level directory")).append("</A></td></tr></thead>\n");
if (showSaveButton) {
buf.append("<thead><tr><th colspan=\"3\">&nbsp;</th><th align=\"center\"><input type=\"submit\" value=\"");
buf.append(_("Save priorities"));
buf.append("\" name=\"foo\" ></th></tr></thead>\n");
}
buf.append("</TABLE>\n");
buf.append("</div></div></center></BODY></HTML>\n");
if (showPriority)
buf.append("</form>");
buf.append("</div></div></BODY></HTML>\n");
return buf.toString();
}
@ -1515,6 +1562,26 @@ buf.append("</div></div></center></BODY></HTML>\n");
return "<img alt=\"\" height=\"16\" width=\"16\" src=\"/i2psnark/_icons/" + icon + ".png\"> ";
}
/** @since 0.8.1 */
private void savePriorities(Snark snark, Map postParams) {
Set<Map.Entry> entries = postParams.entrySet();
for (Map.Entry entry : entries) {
String key = (String)entry.getKey();
if (key.startsWith("pri.")) {
try {
String file = key.substring(4);
String val = ((String[])entry.getValue())[0]; // jetty arrays
int pri = Integer.parseInt(val);
snark.storage.setPriority(file, pri);
//System.err.println("Priority now " + pri + " for " + file);
} catch (Throwable t) { t.printStackTrace(); }
}
}
if (snark.coordinator != null)
snark.coordinator.updatePiecePriorities();
_manager.saveTorrentStatus(snark.storage.getMetaInfo(), snark.storage.getBitField(), snark.storage.getFilePriorities());
}
/** inner class, don't bother reindenting */
private static class FetchAndAdd implements Runnable {