forked from I2P_Developers/i2p.i2p
i2psnark:
- Support arbitrary location for torrent data. Save location in per-torrent config file. TODO: Fix torrent browse pages (ticket #1028) - Enhance idle shutdown message - Javadocs
This commit is contained in:
@ -66,7 +66,8 @@ class IdleChecker extends SimpleTimer2.TimedEvent {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Closing tunnels on idle");
|
||||
_util.disconnect();
|
||||
_mgr.addMessage(_util.getString("I2P tunnel closed."));
|
||||
_mgr.addMessage(_util.getString("No more torrents running.") + ' ' +
|
||||
_util.getString("I2P tunnel closed."));
|
||||
schedule(3 * CHECK_TIME);
|
||||
return;
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ class PeerState implements DataLoader
|
||||
_log.debug(peer + " rcv bitfield");
|
||||
if (bitfield != null)
|
||||
{
|
||||
// XXX - Be liberal in what you except?
|
||||
// XXX - Be liberal in what you accept?
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Got unexpected bitfield message from " + peer);
|
||||
return;
|
||||
|
@ -34,6 +34,7 @@ import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PServerSocket;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SecureFile;
|
||||
|
||||
/**
|
||||
* Main Snark program startup class.
|
||||
@ -238,13 +239,21 @@ public class Snark
|
||||
private volatile boolean _autoStoppable;
|
||||
|
||||
|
||||
/** from main() via parseArguments() single torrent */
|
||||
/**
|
||||
* from main() via parseArguments() single torrent
|
||||
*
|
||||
* @deprecated unused
|
||||
*/
|
||||
Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
|
||||
StorageListener slistener, CoordinatorListener clistener) {
|
||||
this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, ".");
|
||||
}
|
||||
|
||||
/** single torrent - via router */
|
||||
/**
|
||||
* single torrent - via router
|
||||
*
|
||||
* @deprecated unused
|
||||
*/
|
||||
public Snark(I2PAppContext ctx, Properties opts, String torrent,
|
||||
StorageListener slistener, boolean start, String rootDir) {
|
||||
this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
|
||||
@ -275,11 +284,28 @@ public class Snark
|
||||
this.startTorrent();
|
||||
}
|
||||
|
||||
/** multitorrent */
|
||||
/**
|
||||
* multitorrent
|
||||
*/
|
||||
public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
|
||||
StorageListener slistener, CoordinatorListener clistener,
|
||||
CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
|
||||
ConnectionAcceptor connectionAcceptor, boolean start, String rootDir)
|
||||
{
|
||||
this(util, torrent, ip, user_port, slistener, clistener, complistener,
|
||||
peerCoordinatorSet, connectionAcceptor, start, rootDir, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* multitorrent
|
||||
*
|
||||
* @param baseFile if null, use rootDir/torrentName; if non-null, use it instead
|
||||
* @since 0.9.11
|
||||
*/
|
||||
public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
|
||||
StorageListener slistener, CoordinatorListener clistener,
|
||||
CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
|
||||
ConnectionAcceptor connectionAcceptor, boolean start, String rootDir, File baseFile)
|
||||
{
|
||||
if (slistener == null)
|
||||
slistener = this;
|
||||
@ -395,7 +421,14 @@ public class Snark
|
||||
try
|
||||
{
|
||||
activity = "Checking storage";
|
||||
storage = new Storage(_util, rootDataDir, meta, slistener);
|
||||
if (baseFile == null) {
|
||||
String base = Storage.filterName(meta.getName());
|
||||
if (_util.getFilesPublic())
|
||||
baseFile = new File(rootDataDir, base);
|
||||
else
|
||||
baseFile = new SecureFile(rootDataDir, base);
|
||||
}
|
||||
storage = new Storage(_util, baseFile, meta, slistener);
|
||||
if (completeListener != null) {
|
||||
storage.check(completeListener.getSavedTorrentTime(this),
|
||||
completeListener.getSavedTorrentBitField(this));
|
||||
@ -1102,8 +1135,14 @@ public class Snark
|
||||
*/
|
||||
public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
|
||||
try {
|
||||
String base = Storage.filterName(metainfo.getName());
|
||||
File baseFile;
|
||||
if (_util.getFilesPublic())
|
||||
baseFile = new File(rootDataDir, base);
|
||||
else
|
||||
baseFile = new SecureFile(rootDataDir, base);
|
||||
// The following two may throw IOE...
|
||||
storage = new Storage(_util, rootDataDir, metainfo, this);
|
||||
storage = new Storage(_util, baseFile, metainfo, this);
|
||||
storage.check();
|
||||
// ... so don't set meta until here
|
||||
meta = metainfo;
|
||||
|
@ -86,6 +86,7 @@ public class SnarkManager implements CompleteListener {
|
||||
public static final String PROP_DIR = "i2psnark.dir";
|
||||
private static final String PROP_META_PREFIX = "i2psnark.zmeta.";
|
||||
private static final String PROP_META_STAMP = "stamp";
|
||||
private static final String PROP_META_BASE = "base";
|
||||
private static final String PROP_META_BITFIELD = "bitfield";
|
||||
private static final String PROP_META_PRIORITY = "priority";
|
||||
private static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
|
||||
@ -347,7 +348,7 @@ public class SnarkManager implements CompleteListener {
|
||||
*
|
||||
* @return the new config directory, non-null
|
||||
* @throws RuntimeException on creation fail
|
||||
* @since 0.9.10
|
||||
* @since 0.9.11
|
||||
*/
|
||||
private File migrateConfig(File oldFile) {
|
||||
File dir = new SecureDirectory(oldFile + CONFIG_DIR_SUFFIX);
|
||||
@ -455,7 +456,7 @@ public class SnarkManager implements CompleteListener {
|
||||
/**
|
||||
* The config for a torrent
|
||||
* @return non-null, possibly empty
|
||||
* @since 0.9.10
|
||||
* @since 0.9.11
|
||||
*/
|
||||
private Properties getConfig(Snark snark) {
|
||||
return getConfig(snark.getInfoHash());
|
||||
@ -465,7 +466,7 @@ public class SnarkManager implements CompleteListener {
|
||||
* The config for a torrent
|
||||
* @param ih 20-byte infohash
|
||||
* @return non-null, possibly empty
|
||||
* @since 0.9.10
|
||||
* @since 0.9.11
|
||||
*/
|
||||
private Properties getConfig(byte[] ih) {
|
||||
Properties rv = new OrderedProperties();
|
||||
@ -482,7 +483,7 @@ public class SnarkManager implements CompleteListener {
|
||||
* The config file for a torrent
|
||||
* @param confDir the config directory
|
||||
* @param ih 20-byte infohash
|
||||
* @since 0.9.10
|
||||
* @since 0.9.11
|
||||
*/
|
||||
private static File configFile(File confDir, byte[] ih) {
|
||||
String hex = I2PSnarkUtil.toHex(ih);
|
||||
@ -1071,15 +1072,23 @@ public class SnarkManager implements CompleteListener {
|
||||
|
||||
/**
|
||||
* Caller must verify this torrent is not already added.
|
||||
*
|
||||
* @param filename the absolute path to save the metainfo to, generally ending in ".torrent"
|
||||
* @param baseFile may be null, if so look in rootDataDir
|
||||
* @throws RuntimeException via Snark.fatal()
|
||||
*/
|
||||
private void addTorrent(String filename) { addTorrent(filename, false); }
|
||||
private void addTorrent(String filename) {
|
||||
addTorrent(filename, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caller must verify this torrent is not already added.
|
||||
*
|
||||
* @param filename the absolute path to save the metainfo to, generally ending in ".torrent"
|
||||
* @param baseFile may be null, if so look in rootDataDir
|
||||
* @throws RuntimeException via Snark.fatal()
|
||||
*/
|
||||
private void addTorrent(String filename, boolean dontAutoStart) {
|
||||
private void addTorrent(String filename, File baseFile, boolean dontAutoStart) {
|
||||
if ((!dontAutoStart) && !_util.connected()) {
|
||||
addMessage(_("Connecting to I2P"));
|
||||
boolean ok = _util.connect();
|
||||
@ -1160,9 +1169,13 @@ public class SnarkManager implements CompleteListener {
|
||||
} else {
|
||||
// TODO load saved closest DHT nodes and pass to the Snark ?
|
||||
// This may take a LONG time
|
||||
if (baseFile == null)
|
||||
baseFile = getSavedBaseFile(info.getInfoHash());
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("New Snark, torrent: " + filename + " base: " + baseFile);
|
||||
torrent = new Snark(_util, filename, null, -1, null, null, this,
|
||||
_peerCoordinatorSet, _connectionAcceptor,
|
||||
false, dataDir.getPath());
|
||||
false, dataDir.getPath(), baseFile);
|
||||
loadSavedFilePriorities(torrent);
|
||||
synchronized (_snarks) {
|
||||
_snarks.put(filename, torrent);
|
||||
@ -1305,14 +1318,17 @@ public class SnarkManager implements CompleteListener {
|
||||
* This verifies that a torrent with this infohash is not already added.
|
||||
* This may take a LONG time to create or check the storage.
|
||||
*
|
||||
* Called from servlet.
|
||||
*
|
||||
* @param metainfo the metainfo for the torrent
|
||||
* @param bitfield the current completion status of the torrent
|
||||
* @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent
|
||||
* Must be a filesystem-safe name.
|
||||
* @param baseFile may be null, if so look in rootDataDir
|
||||
* @throws RuntimeException via Snark.fatal()
|
||||
* @since 0.8.4
|
||||
*/
|
||||
public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException {
|
||||
public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, File baseFile, boolean dontAutoStart) throws IOException {
|
||||
// prevent interference by DirMonitor
|
||||
synchronized (_snarks) {
|
||||
Snark snark = getTorrentByInfoHash(metainfo.getInfoHash());
|
||||
@ -1321,11 +1337,11 @@ public class SnarkManager implements CompleteListener {
|
||||
return;
|
||||
}
|
||||
// so addTorrent won't recheck
|
||||
saveTorrentStatus(metainfo, bitfield, null); // no file priorities
|
||||
saveTorrentStatus(metainfo, bitfield, null, baseFile); // no file priorities
|
||||
try {
|
||||
locked_writeMetaInfo(metainfo, filename, areFilesPublic());
|
||||
// hold the lock for a long time
|
||||
addTorrent(filename, dontAutoStart);
|
||||
addTorrent(filename, baseFile, dontAutoStart);
|
||||
} catch (IOException ioe) {
|
||||
addMessage(_("Failed to copy torrent file to {0}", filename));
|
||||
_log.error("Failed to write torrent file", ioe);
|
||||
@ -1461,6 +1477,19 @@ public class SnarkManager implements CompleteListener {
|
||||
}
|
||||
storage.setFilePriorities(rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base location for a torrent from the config file.
|
||||
* @return File or null, doesn't necessarily exist
|
||||
* @since 0.9.11
|
||||
*/
|
||||
public File getSavedBaseFile(byte[] ih) {
|
||||
Properties config = getConfig(ih);
|
||||
String base = config.getProperty(PROP_META_BASE);
|
||||
if (base == null)
|
||||
return null;
|
||||
return new File(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the completion status of a torrent and the current time in the config file
|
||||
@ -1471,14 +1500,15 @@ public class SnarkManager implements CompleteListener {
|
||||
*
|
||||
* @param bitfield non-null
|
||||
* @param priorities may be null
|
||||
* @param base may be null
|
||||
*/
|
||||
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
|
||||
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) {
|
||||
synchronized (_configLock) {
|
||||
locked_saveTorrentStatus(metainfo, bitfield, priorities);
|
||||
locked_saveTorrentStatus(metainfo, bitfield, priorities, base);
|
||||
}
|
||||
}
|
||||
|
||||
private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
|
||||
private void locked_saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities, File base) {
|
||||
byte[] ih = metainfo.getInfoHash();
|
||||
String bfs;
|
||||
if (bitfield.complete()) {
|
||||
@ -1490,6 +1520,8 @@ public class SnarkManager implements CompleteListener {
|
||||
Properties config = getConfig(ih);
|
||||
config.setProperty(PROP_META_STAMP, Long.toString(System.currentTimeMillis()));
|
||||
config.setProperty(PROP_META_BITFIELD, bfs);
|
||||
if (base != null)
|
||||
config.setProperty(PROP_META_BASE, base.getAbsolutePath());
|
||||
|
||||
// now the file priorities
|
||||
if (priorities != null) {
|
||||
@ -1742,7 +1774,7 @@ public class SnarkManager implements CompleteListener {
|
||||
MetaInfo meta = snark.getMetaInfo();
|
||||
Storage storage = snark.getStorage();
|
||||
if (meta != null && storage != null)
|
||||
saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities());
|
||||
saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities(), storage.getBase());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1764,7 +1796,7 @@ public class SnarkManager implements CompleteListener {
|
||||
snark.stopTorrent();
|
||||
return null;
|
||||
}
|
||||
saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities
|
||||
saveTorrentStatus(meta, storage.getBitField(), null, storage.getBase()); // no file priorities
|
||||
// temp for addMessage() in case canonical throws
|
||||
String name = storage.getBaseName();
|
||||
try {
|
||||
@ -1865,7 +1897,7 @@ public class SnarkManager implements CompleteListener {
|
||||
try {
|
||||
// Snark.fatal() throws a RuntimeException
|
||||
// don't let one bad torrent kill the whole loop
|
||||
addTorrent(name, !shouldAutoStart());
|
||||
addTorrent(name, null, !shouldAutoStart());
|
||||
} catch (Exception e) {
|
||||
addMessage(_("Error: Could not add the torrent {0}", name) + ": " + e);
|
||||
_log.error("Unable to add the torrent " + name, e);
|
||||
|
@ -86,20 +86,18 @@ public class Storage
|
||||
private static final ByteCache _cache = ByteCache.getInstance(16, BUFSIZE);
|
||||
|
||||
/**
|
||||
* Creates a new storage based on the supplied MetaInfo. This will
|
||||
* Creates a new storage based on the supplied MetaInfo.
|
||||
*
|
||||
* Does not check storage. Caller MUST call check(), which will
|
||||
* try to create and/or check all needed files in the MetaInfo.
|
||||
*
|
||||
* Does not check storage. Caller MUST call check()
|
||||
* @param baseFile the torrent data file or dir
|
||||
*/
|
||||
public Storage(I2PSnarkUtil util, File rootDir, MetaInfo metainfo, StorageListener listener)
|
||||
public Storage(I2PSnarkUtil util, File baseFile, MetaInfo metainfo, StorageListener listener)
|
||||
{
|
||||
_util = util;
|
||||
_log = util.getContext().logManager().getLog(Storage.class);
|
||||
boolean areFilesPublic = _util.getFilesPublic();
|
||||
if (areFilesPublic)
|
||||
_base = new File(rootDir, filterName(metainfo.getName()));
|
||||
else
|
||||
_base = new SecureFile(rootDir, filterName(metainfo.getName()));
|
||||
_base = baseFile;
|
||||
this.metainfo = metainfo;
|
||||
this.listener = listener;
|
||||
needed = metainfo.getPieces();
|
||||
@ -708,7 +706,8 @@ public class Storage
|
||||
|
||||
/**
|
||||
* The base file or directory.
|
||||
* @return a new List
|
||||
* @return the File
|
||||
* @since 0.9.11
|
||||
*/
|
||||
public File getBase() {
|
||||
return _base;
|
||||
@ -716,8 +715,8 @@ public class Storage
|
||||
|
||||
/**
|
||||
* Does not include directories. Unsorted.
|
||||
* @since 0.9.10
|
||||
* @return a new List
|
||||
* @since 0.9.11
|
||||
*/
|
||||
public List<File> getFiles() {
|
||||
List<File> rv = new ArrayList<File>(_torrentFiles.size());
|
||||
@ -731,7 +730,7 @@ public class Storage
|
||||
* Includes the base for a multi-file torrent.
|
||||
* Sorted bottom-up for easy deletion.
|
||||
* Slow. Use for deletion only.
|
||||
* @since 0.9.10
|
||||
* @since 0.9.11
|
||||
* @return a new Set or null for a single-file torrent
|
||||
*/
|
||||
public SortedSet<File> getDirectories() {
|
||||
|
@ -896,7 +896,9 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
} else if ("Create".equals(action)) {
|
||||
String baseData = req.getParameter("baseFile");
|
||||
if (baseData != null && baseData.trim().length() > 0) {
|
||||
File baseFile = new File(_manager.getDataDir(), baseData);
|
||||
File baseFile = new File(baseData.trim());
|
||||
if (!baseFile.isAbsolute())
|
||||
baseFile = new File(_manager.getDataDir(), baseData);
|
||||
String announceURL = req.getParameter("announceURL");
|
||||
// make the user add a tracker on the config form now
|
||||
//String announceURLOther = req.getParameter("announceURLOther");
|
||||
@ -956,7 +958,7 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
|
||||
// FIXME is the storage going to stay around thanks to the info reference?
|
||||
// now add it, but don't automatically start it
|
||||
_manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true);
|
||||
_manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), baseFile, true);
|
||||
_manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
|
||||
if (announceURL != null && !_manager.util().getOpenTrackers().contains(announceURL))
|
||||
_manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
|
||||
@ -1708,10 +1710,11 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
out.write("</span><hr>\n<table border=\"0\"><tr><td>");
|
||||
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n");
|
||||
out.write(_("Data to seed"));
|
||||
out.write(":<td><code>" + _manager.getDataDir().getAbsolutePath() + File.separatorChar
|
||||
+ "</code><input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile
|
||||
out.write(":<td>"
|
||||
+ "<input type=\"text\" name=\"baseFile\" size=\"58\" value=\"" + baseFile
|
||||
+ "\" spellcheck=\"false\" title=\"");
|
||||
out.write(_("File or directory to seed (must be within the specified path)"));
|
||||
out.write(_("File or directory to seed (full path or within the directory {0} )",
|
||||
_manager.getDataDir().getAbsolutePath() + File.separatorChar));
|
||||
out.write("\" ><tr><td>\n");
|
||||
out.write(_("Trackers"));
|
||||
out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">");
|
||||
@ -2198,12 +2201,6 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
private String getListHTML(File r, String base, boolean parent, Map<String, String[]> postParams)
|
||||
throws IOException
|
||||
{
|
||||
File[] ls = null;
|
||||
if (r.isDirectory()) {
|
||||
ls = r.listFiles();
|
||||
Arrays.sort(ls, new ListingComparator());
|
||||
} // if r is not a directory, we are only showing torrent info section
|
||||
|
||||
String title = decodePath(base);
|
||||
String cpath = _contextPath + '/';
|
||||
if (title.startsWith(cpath))
|
||||
@ -2249,7 +2246,8 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
|
||||
if (parent) // always true
|
||||
buf.append("<div class=\"page\"><div class=\"mainsection\">");
|
||||
boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete();
|
||||
boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
|
||||
r.isDirectory();
|
||||
if (showPriority) {
|
||||
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
|
||||
buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n");
|
||||
@ -2271,6 +2269,12 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
.append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
|
||||
.append(fullPath)
|
||||
.append("</a></td></tr>\n");
|
||||
buf.append("<tr><td>")
|
||||
.append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>")
|
||||
.append(_("Data location"))
|
||||
.append(":</b> ")
|
||||
.append(urlEncode(snark.getStorage().getBase().getPath()))
|
||||
.append("</td></tr>\n");
|
||||
|
||||
MetaInfo meta = snark.getMetaInfo();
|
||||
if (meta != null) {
|
||||
@ -2404,6 +2408,22 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
.append("\"</th></tr>\n");
|
||||
}
|
||||
buf.append("</table>\n");
|
||||
|
||||
if (snark != null && !r.exists()) {
|
||||
// fixup TODO
|
||||
buf.append("<p>Does not exist<br>resource=\"").append(r.toString())
|
||||
.append("\"<br>base=\"").append(base)
|
||||
.append("\"<br>torrent=\"").append(torrentName)
|
||||
.append("\"</p></div></div></BODY></HTML>");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
File[] ls = null;
|
||||
if (r.isDirectory()) {
|
||||
ls = r.listFiles();
|
||||
Arrays.sort(ls, new ListingComparator());
|
||||
} // if r is not a directory, we are only showing torrent info section
|
||||
|
||||
if (ls == null) {
|
||||
// We are only showing the torrent info section
|
||||
buf.append("</div></div></BODY></HTML>");
|
||||
@ -2655,6 +2675,6 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
}
|
||||
}
|
||||
snark.updatePiecePriorities();
|
||||
_manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities());
|
||||
_manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities(), storage.getBase());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user