358 lines
11 KiB
Java
358 lines
11 KiB
Java
|
package org.klomp.snark.rpc;
|
||
|
|
||
|
/*
|
||
|
* 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.data.DataHelper;
|
||
|
import net.i2p.util.EepGet;
|
||
|
import net.i2p.util.I2PAppThread;
|
||
|
import net.i2p.util.Log;
|
||
|
import net.i2p.util.SecureFile;
|
||
|
|
||
|
import org.klomp.snark.I2PSnarkUtil;
|
||
|
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 final File _dataDir;
|
||
|
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.
|
||
|
*
|
||
|
* @param dataDir null to default to snark data directory
|
||
|
*/
|
||
|
public FetchAndAdd(I2PAppContext ctx, SnarkManager mgr, String url, File dataDir) {
|
||
|
// 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 = _t("Download torrent file from {0}", url);
|
||
|
_dataDir = dataDir;
|
||
|
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() {
|
||
|
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.addMessageNoEscape(_t("Torrent was not retrieved from {0}", urlify(_url)) +
|
||
|
((_failCause != null) ? (": " + DataHelper.stripHTML(_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(_t("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);
|
||
|
_eepGet.addHeader("User-Agent", I2PSnarkUtil.EEPGET_USER_AGENT);
|
||
|
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) {
|
||
|
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(_t("Torrent with this info hash is already running: {0}", snark.getBaseName()));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
String originalName = Storage.filterName(name);
|
||
|
name = originalName + ".torrent";
|
||
|
File torrentFile = new File(_mgr.getDataDir(), name);
|
||
|
|
||
|
String canonical = torrentFile.getCanonicalPath();
|
||
|
|
||
|
if (torrentFile.exists()) {
|
||
|
if (_mgr.getTorrent(canonical) != null)
|
||
|
_mgr.addMessage(_t("Torrent already running: {0}", name));
|
||
|
else
|
||
|
_mgr.addMessage(_t("Torrent already in the queue: {0}", name));
|
||
|
} else {
|
||
|
// This may take a LONG time to create the storage.
|
||
|
_mgr.copyAndAddTorrent(file, canonical, _dataDir);
|
||
|
snark = _mgr.getTorrentByBaseName(originalName);
|
||
|
if (snark != null)
|
||
|
snark.startTorrent();
|
||
|
else
|
||
|
throw new IOException("Unknown error - check logs");
|
||
|
}
|
||
|
} catch (IOException ioe) {
|
||
|
_mgr.addMessageNoEscape(_t("Torrent at {0} was not valid", urlify(_url)) + ": " + DataHelper.stripHTML(ioe.getMessage()));
|
||
|
} catch (OutOfMemoryError oom) {
|
||
|
_mgr.addMessageNoEscape(_t("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + DataHelper.stripHTML(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 = _util.getContext().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 -1 when done so the web will list us as "complete" instead of "seeding"
|
||
|
*/
|
||
|
@Override
|
||
|
public long getRemainingLength() {
|
||
|
long rv = _remaining;
|
||
|
return rv > 0 ? rv : -1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @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 _t(String s) {
|
||
|
return _mgr.util().getString(s);
|
||
|
}
|
||
|
|
||
|
private String _t(String s, String o) {
|
||
|
return _mgr.util().getString(s, o);
|
||
|
}
|
||
|
|
||
|
private static String urlify(String s) {
|
||
|
return UIUtil.urlify(s);
|
||
|
}
|
||
|
}
|