diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 8344469bc8b76e4e7b214bb9fbb0f7cdd8c2b881..51952d39d7102b22402a39008008d62dbc03694e 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -1119,21 +1119,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
+     *  @param baseFile may be null, if so look in dataDir
      *  @throws RuntimeException via Snark.fatal()
      */
-    private void addTorrent(String filename) {
-        addTorrent(filename, null, false);
+    private void addTorrent(String filename, File baseFile, boolean dontAutoStart) {
+        addTorrent(filename, baseFile, dontAutoStart, null);
     }
 
     /**
      *  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
+     *  @param baseFile may be null, if so look in dataDir
+     *  @param dataDir must exist, or null to default to snark data directory
      *  @throws RuntimeException via Snark.fatal()
+     *  @since 0.9.17
      */
-    private void addTorrent(String filename, File baseFile, boolean dontAutoStart) {
+    private void addTorrent(String filename, File baseFile, boolean dontAutoStart, File dataDir) {
         if ((!dontAutoStart) && !_util.connected()) {
             addMessage(_("Connecting to I2P"));
             boolean ok = _util.connect();
@@ -1150,7 +1152,8 @@ public class SnarkManager implements CompleteListener {
             addMessage(_("Error: Could not add the torrent {0}", filename) + ": " + ioe);
             return;
         }
-        File dataDir = getDataDir();
+        if (dataDir == null)
+            dataDir = getDataDir();
         Snark torrent = null;
         synchronized (_snarks) {
             torrent = _snarks.get(filename);
@@ -1266,7 +1269,25 @@ public class SnarkManager implements CompleteListener {
      */
     public void addMagnet(String name, byte[] ih, String trackerURL, boolean updateStatus) {
         // updateStatus is true from UI, false from config file bulk add
-        addMagnet(name, ih, trackerURL, updateStatus, updateStatus, this);
+        addMagnet(name, ih, trackerURL, updateStatus, updateStatus, null, this);
+    }
+    
+    /**
+     * Add a torrent with the info hash alone (magnet / maggot)
+     *
+     * @param name hex or b32 name from the magnet link
+     * @param ih 20 byte info hash
+     * @param trackerURL may be null
+     * @param updateStatus should we add this magnet to the config file,
+     *                     to save it across restarts, in case we don't get
+     *                     the metadata before shutdown?
+     * @param dataDir must exist, or null to default to snark data directory
+     * @throws RuntimeException via Snark.fatal()
+     * @since 0.9.17
+     */
+    public void addMagnet(String name, byte[] ih, String trackerURL, boolean updateStatus, File dataDir) {
+        // updateStatus is true from UI, false from config file bulk add
+        addMagnet(name, ih, trackerURL, updateStatus, updateStatus, dataDir, this);
     }
     
     /**
@@ -1279,16 +1300,18 @@ public class SnarkManager implements CompleteListener {
      * @param updateStatus should we add this magnet to the config file,
      *                     to save it across restarts, in case we don't get
      *                     the metadata before shutdown?
+     * @param dataDir must exist, or null to default to snark data directory
      * @param listener to intercept callbacks, should pass through to this
      * @return the new Snark or null on failure
      * @throws RuntimeException via Snark.fatal()
      * @since 0.9.4
      */
     public Snark addMagnet(String name, byte[] ih, String trackerURL, boolean updateStatus,
-                          boolean autoStart, CompleteListener listener) {
+                          boolean autoStart, File dataDir, CompleteListener listener) {
+        String dirPath = dataDir != null ? dataDir.getAbsolutePath() : getDataDir().getPath();
         Snark torrent = new Snark(_util, name, ih, trackerURL, listener,
                                   _peerCoordinatorSet, _connectionAcceptor,
-                                  false, getDataDir().getPath());
+                                  false, dirPath);
 
         synchronized (_snarks) {
             Snark snark = getTorrentByInfoHash(ih);
@@ -1407,10 +1430,11 @@ public class SnarkManager implements CompleteListener {
      * @param fromfile where the file is now, presumably in a temp directory somewhere
      * @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 dataDir must exist, or null to default to snark data directory
      * @throws RuntimeException via Snark.fatal()
      * @since 0.8.4
      */
-    public void copyAndAddTorrent(File fromfile, String filename) throws IOException {
+    public void copyAndAddTorrent(File fromfile, String filename, File dataDir) throws IOException {
         // prevent interference by DirMonitor
         synchronized (_snarks) {
             boolean success = FileUtil.copy(fromfile.getAbsolutePath(), filename, false);
@@ -1422,7 +1446,7 @@ public class SnarkManager implements CompleteListener {
             if (!areFilesPublic())
                 SecureFileOutputStream.setPerms(new File(filename));
             // hold the lock for a long time
-            addTorrent(filename);
+            addTorrent(filename, null, false, dataDir);
          }
     }
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java b/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
index d7f3b6690842119bf00358a18f6b3f73e91d1d13..4f9dff55aad18cb39ebbe444529c78cb49b75495 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
@@ -110,7 +110,7 @@ class UpdateRunner implements UpdateTask, CompleteListener {
                     _umgr.notifyAttemptFailed(this, "No tracker, no DHT, no OT", null);
                     continue;
                 }
-                _snark = _smgr.addMagnet(name, ih, trackerURL, true, true, this);
+                _snark = _smgr.addMagnet(name, ih, trackerURL, true, true, null, this);
                 if (_snark != null) {
                     updateStatus("<b>" + _smgr.util().getString("Updating from {0}", linkify(updateURL)) + "</b>");
                     new Timeout();
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java b/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java
index d24d4f87c88b9322c2cec02c7318281109537e9d..99e2f62a694c293eb7bfc88d04ff1f56c92e41ee 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/FetchAndAdd.java
@@ -50,6 +50,7 @@ public class FetchAndAdd extends Snark implements EepGet.StatusListener, Runnabl
     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;
@@ -65,8 +66,10 @@ public class FetchAndAdd extends Snark implements EepGet.StatusListener, Runnabl
     /**
      *   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) {
+    public FetchAndAdd(I2PAppContext ctx, SnarkManager mgr, String url, File dataDir) {
         // magnet constructor
         super(mgr.util(), "Torrent download",
               null, null, null, null, null, false, null);
@@ -75,6 +78,7 @@ public class FetchAndAdd extends Snark implements EepGet.StatusListener, Runnabl
         _mgr = mgr;
         _url = url;
         _name = _("Download torrent file from {0}", url);
+        _dataDir = dataDir;
         byte[] fake = null;
         try {
             fake = SHA1.getInstance().digest(url.getBytes("ISO-8859-1"));
@@ -176,7 +180,7 @@ public class FetchAndAdd extends Snark implements EepGet.StatusListener, Runnabl
                     _mgr.addMessage(_("Torrent already in the queue: {0}", name));
             } else {
                 // This may take a LONG time to create the storage.
-                _mgr.copyAndAddTorrent(file, canonical);
+                _mgr.copyAndAddTorrent(file, canonical, _dataDir);
                 snark = _mgr.getTorrentByBaseName(originalName);
                 if (snark != null)
                     snark.startTorrent();
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 4276679e7dd51df1aa941209d16465e84dd0db4e..a085864c17ef1ccb1d12ff1f2a86cf68e05fc36c 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -30,6 +30,7 @@ import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFile;
 
 import org.klomp.snark.I2PSnarkUtil;
 import org.klomp.snark.MagnetURI;
@@ -933,13 +934,41 @@ public class I2PSnarkServlet extends BasicServlet {
             } else
           *****/
             if (newURL != null) {
+                String newDir = req.getParameter("nofilter_newDir");
+                File dir = null;
+                if (newDir != null) {
+                    newDir = newDir.trim();
+                    if (newDir.length() > 0) {
+                        dir = new SecureFile(newDir);
+                        if (!dir.isAbsolute()) {
+                            _manager.addMessage(_("Data directory must be an absolute path") + ": " + dir);
+                            return;
+                        }
+                        if (!dir.isDirectory() && !dir.mkdirs()) {
+                            _manager.addMessage(_("Data directory cannot be created") + ": " + dir);
+                            return;
+                        }
+                        Collection<Snark> snarks = _manager.getTorrents();
+                        for (Snark s : snarks) {
+                            Storage storage = s.getStorage();
+                            if (storage == null)
+                                continue;
+                            File sbase = storage.getBase();
+                            if (isParentOf(sbase, dir)) {
+                                _manager.addMessage(_("Cannot add torrent {0} inside another torrent: {1}",
+                                                      dir.getAbsolutePath(), sbase));
+                                return;
+                            }
+                        }
+                    }
+                }
                 if (newURL.startsWith("http://")) {
-                    FetchAndAdd fetch = new FetchAndAdd(_context, _manager, newURL);
+                    FetchAndAdd fetch = new FetchAndAdd(_context, _manager, newURL, dir);
                     _manager.addDownloader(fetch);
                 } else if (newURL.startsWith(MagnetURI.MAGNET) || newURL.startsWith(MagnetURI.MAGGOT)) {
-                    addMagnet(newURL);
+                    addMagnet(newURL, dir);
                 } else if (newURL.length() == 40 && newURL.replaceAll("[a-fA-F0-9]", "").length() == 0) {
-                    addMagnet(MagnetURI.MAGNET_FULL + newURL);
+                    addMagnet(MagnetURI.MAGNET_FULL + newURL, dir);
                 } else {
                     _manager.addMessage(_("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"",
                                           MagnetURI.MAGNET, MagnetURI.MAGGOT));
@@ -2000,6 +2029,7 @@ public class I2PSnarkServlet extends BasicServlet {
         out.write(toThemeImg("add"));
         out.write(' ');
         out.write(_("Add Torrent"));
+
         out.write("</span><hr>\n<table border=\"0\"><tr><td>");
         out.write(_("From URL"));
         out.write(":<td><input type=\"text\" name=\"nofilter_newURL\" size=\"85\" value=\"" + newURL + "\" spellcheck=\"false\"");
@@ -2010,9 +2040,17 @@ public class I2PSnarkServlet extends BasicServlet {
         //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>");
         out.write("<input type=\"submit\" class=\"add\" value=\"");
         out.write(_("Add torrent"));
-        out.write("\" name=\"foo\" ><br>\n");
+        out.write("\" name=\"foo\" ><br>\n" +
+
+                  "<tr><td>");
+        out.write(_("Data dir"));
+        out.write(":<td><input type=\"text\" name=\"nofilter_newDir\" size=\"85\" value=\"\" spellcheck=\"false\"");
+        out.write(" title=\"");
+        out.write(_("Enter the directory to save the data in (default {0})", _manager.getDataDir().getAbsolutePath()));
+        out.write("\"></td></tr>\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");
@@ -2372,15 +2410,16 @@ public class I2PSnarkServlet extends BasicServlet {
 
     /**
      *  @param url in base32 or hex
+     *  @param dataDir null to default to snark data directory
      *  @since 0.8.4
      */
-    private void addMagnet(String url) {
+    private void addMagnet(String url, File dataDir) {
         try {
             MagnetURI magnet = new MagnetURI(_manager.util(), url);
             String name = magnet.getName();
             byte[] ih = magnet.getInfoHash();
             String trackerURL = magnet.getTrackerURL();
-            _manager.addMagnet(name, ih, trackerURL, true);
+            _manager.addMagnet(name, ih, trackerURL, true, dataDir);
         } catch (IllegalArgumentException iae) {
             _manager.addMessage(_("Invalid magnet URL {0}", url));
         }