From 524a25eb2c5cdab6e3923a0748df9068174fcdfb Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Thu, 4 Jun 2009 19:14:40 +0000
Subject: [PATCH] Big directory rework.

Eliminate all uses of the current working directory, and
set up multiple directories specified by absolute paths for various uses.

Add a WorkingDir class to create a user config directory and
migrate files to it for new installs.
The directory will be $HOME/.i2p on linux and %APPDIR%\I2P on Windows,
or as specified in the system property -Di2p.dir.config=/path/to/i2pdir
All files except for the base install and temp files will be
in the config directory by default.
Temp files will be in a i2p-xxxxx subdirectory of the system temp directory
specified by the system property java.io.tmpdir.

Convert all file opens in the code to be relative to a specific directory,
as specified in the context. Code and applications should never open
files relative to the current working directory (e.g. new File("foo")).
All files should be accessed in the appropriate context directory,
e.g. new File(_context.getAppDir(), "foo").

The router.config file location may be specified as a system property on the
java command line with -Drouter.configLocation=/path/to/router.config
All directories may be specified as properties in the router.config file.

The migration will copy all files from an existing installation,
except i2psnark/, with the system property -Di2p.dir.migrate=true.
Otherwise it will just set up a new directory with a minimal configuration.

The migration will also create a modified wrapper.config and (on linux only)
a modified i2prouter script, and place them in the config directory.

There are no changes to the installer or the default i2prouter, i2prouter.bat,
i2prouter, wrapper.config, runplain.sh, windows service installer/uninstaller,
etc. in this checkin.


    *  Directories. These are all set at instantiation and will not be changed by
    *  subsequent property changes.
    *  All properties, if set, should be absolute paths.
    *
    *  Name	Property 	Method		Files
    *  -----	-------- 	-----		-----
    *  Base	i2p.dir.base	getBaseDir()	lib/, webapps/, docs/, geoip/, licenses/, ...
    *  Temp	i2p.dir.temp	getTempDir()	Temporary files
    *  Config	i2p.dir.config	getConfigDir()	*.config, hosts.txt, addressbook/, ...
    *
    *  (the following all default to the same as Config)
    *
    *  Router	i2p.dir.router	getRouterDir()	netDb/, peerProfiles/, router.*, keyBackup/, ...
    *  Log	i2p.dir.log	getLogDir()	wrapper.log*, logs/
    *  PID	i2p.dir.pid	getPIDDir()	wrapper *.pid files, router.ping
    *  App	i2p.dir.app	getAppDir()	eepsite/, ...
    *
    *  Note that we can't control where the wrapper actually puts its files.

All these will be set appropriately in a Router Context.
In an I2P App Context, all except Temp will be the current working directory.

Lightly tested so far, needs much more testing.
---
 apps/BOB/src/net/i2p/BOB/BOB.java             |  23 +-
 .../java/src/addressbook/AddressBook.java     |   7 +-
 .../java/src/addressbook/Daemon.java          |  13 +-
 .../src/org/klomp/snark/I2PSnarkUtil.java     |   2 +-
 .../src/org/klomp/snark/SnarkManager.java     |  23 +-
 .../org/klomp/snark/web/I2PSnarkServlet.java  |   1 +
 .../java/src/net/i2p/i2ptunnel/I2PTunnel.java |   8 +
 .../i2p/i2ptunnel/I2PTunnelConnectClient.java |  11 +-
 .../i2p/i2ptunnel/I2PTunnelHTTPClient.java    |  18 +-
 .../net/i2p/i2ptunnel/TunnelController.java   |   2 +
 .../i2p/i2ptunnel/TunnelControllerGroup.java  |   6 +-
 .../i2p/router/web/ConfigServiceHandler.java  |   2 +-
 .../src/net/i2p/router/web/ContentHelper.java |   4 +-
 .../src/net/i2p/router/web/LogsHelper.java    |  70 +--
 .../src/net/i2p/router/web/NewsFetcher.java   |  39 +-
 .../src/net/i2p/router/web/ReseedHandler.java |   2 +-
 .../i2p/router/web/RouterConsoleRunner.java   |  20 +-
 .../src/net/i2p/router/web/UpdateHandler.java |  11 +-
 apps/routerconsole/jsp/flags.jsp              |   5 +-
 apps/routerconsole/jsp/viewtheme.jsp          |   6 +-
 .../src/java/src/i2p/susi/dns/ConfigBean.java |   6 +-
 apps/susimail/build.xml                       |   1 +
 .../src/src/i2p/susi/util/Config.java         |   6 +-
 .../freenet/support/CPUInformation/CPUID.java |   7 +-
 core/java/src/net/i2p/I2PAppContext.java      | 150 ++++++
 .../client/naming/HostsTxtNamingService.java  |   6 +-
 .../src/net/i2p/crypto/TrustedUpdate.java     |  21 +-
 .../java/src/net/i2p/data/PrivateKeyFile.java |   4 +
 .../src/net/i2p/stat/BufferedStatLog.java     |   4 +
 core/java/src/net/i2p/util/FileUtil.java      |   6 +
 core/java/src/net/i2p/util/LogManager.java    |  32 +-
 core/java/src/net/i2p/util/LogWriter.java     |  30 +-
 .../src/net/i2p/util/NativeBigInteger.java    |   5 +-
 core/java/src/net/i2p/util/RandomSource.java  |   4 +-
 core/java/src/net/i2p/util/WorkingDir.java    | 481 ++++++++++++++++++
 router/java/src/net/i2p/router/Blocklist.java |   4 +
 .../java/src/net/i2p/router/KeyManager.java   |   6 +-
 router/java/src/net/i2p/router/Router.java    | 138 ++++-
 .../src/net/i2p/router/RouterContext.java     |   8 +-
 .../kademlia/PersistentDataStore.java         |   2 +-
 .../peermanager/ProfilePersistenceHelper.java |  14 +-
 .../i2p/router/startup/ClientAppConfig.java   |   7 +-
 .../router/startup/CreateRouterInfoJob.java   |  17 +-
 .../i2p/router/startup/LoadRouterInfoJob.java |  16 +-
 .../router/startup/RebuildRouterInfoJob.java  |  29 +-
 .../src/net/i2p/router/transport/GeoIP.java   |   6 +-
 46 files changed, 1039 insertions(+), 244 deletions(-)
 create mode 100644 core/java/src/net/i2p/util/WorkingDir.java

diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java
index 9f87e4f88f..1aa432e2eb 100644
--- a/apps/BOB/src/net/i2p/BOB/BOB.java
+++ b/apps/BOB/src/net/i2p/BOB/BOB.java
@@ -23,6 +23,7 @@
  */
 package net.i2p.BOB;
 
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -34,6 +35,8 @@ import java.net.Socket;
 import java.net.SocketTimeoutException;
 import java.util.Properties;
 import java.util.concurrent.atomic.AtomicBoolean;
+
+import net.i2p.I2PAppContext;
 import net.i2p.client.I2PClient;
 import net.i2p.client.streaming.RetransmissionTimer;
 import net.i2p.util.Log;
@@ -186,16 +189,19 @@ public class BOB {
 		i = Y2.hashCode();
 		try {
 			{
+    				File cfg = new File(configLocation);
+				if (!cfg.isAbsolute())
+					cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), configLocation);
 				try {
-					FileInputStream fi = new FileInputStream(configLocation);
+					FileInputStream fi = new FileInputStream(cfg);
 					props.load(fi);
 					fi.close();
 				} catch (FileNotFoundException fnfe) {
-					warn("Unable to load up the BOB config file " + configLocation + ", Using defaults.");
+					warn("Unable to load up the BOB config file " + cfg.getAbsolutePath() + ", Using defaults.");
 					warn(fnfe.toString());
 					save = true;
 				} catch (IOException ioe) {
-					warn("IOException on BOB config file " + configLocation + ", using defaults.");
+					warn("IOException on BOB config file " + cfg.getAbsolutePath() + ", using defaults.");
 					warn(ioe.toString());
 				}
 			}
@@ -228,13 +234,16 @@ public class BOB {
 				props.setProperty(PROP_BOB_HOST, "localhost");
 			}
 			if (save) {
+    				File cfg = new File(configLocation);
+				if (!cfg.isAbsolute())
+					cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), configLocation);
 				try {
-					warn("Writing new defaults file " + configLocation);
-					FileOutputStream fo = new FileOutputStream(configLocation);
-					props.store(fo, configLocation);
+					warn("Writing new defaults file " + cfg.getAbsolutePath());
+					FileOutputStream fo = new FileOutputStream(cfg);
+					props.store(fo, cfg.getAbsolutePath());
 					fo.close();
 				} catch (IOException ioe) {
-					error("IOException on BOB config file " + configLocation + ", " + ioe);
+					error("IOException on BOB config file " + cfg.getAbsolutePath() + ", " + ioe);
 				}
 			}
 
diff --git a/apps/addressbook/java/src/addressbook/AddressBook.java b/apps/addressbook/java/src/addressbook/AddressBook.java
index a46c256c80..2694cae781 100644
--- a/apps/addressbook/java/src/addressbook/AddressBook.java
+++ b/apps/addressbook/java/src/addressbook/AddressBook.java
@@ -94,20 +94,21 @@ public class AddressBook {
      * @param proxyPort port number of proxy
      */
     public AddressBook(Subscription subscription, String proxyHost, int proxyPort) {
+        File tmp = new File(I2PAppContext.getGlobalContext().getTempDir(), "addressbook.tmp");
         this.location = subscription.getLocation();
         EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
-                proxyHost, proxyPort, 0, -1l, MAX_SUB_SIZE, "addressbook.tmp", null,
+                proxyHost, proxyPort, 0, -1l, MAX_SUB_SIZE, tmp.getAbsolutePath(), null,
                 subscription.getLocation(), true, subscription.getEtag(), subscription.getLastModified(), null);
         if (get.fetch()) {
             subscription.setEtag(get.getETag());
             subscription.setLastModified(get.getLastModified());
         }
         try {            
-            this.addresses = ConfigParser.parse(new File("addressbook.tmp"));
+            this.addresses = ConfigParser.parse(tmp);
         } catch (IOException exp) {
             this.addresses = new HashMap();
         }
-        new File("addressbook.tmp").delete();
+        tmp.delete();
     }
 
     /**
diff --git a/apps/addressbook/java/src/addressbook/Daemon.java b/apps/addressbook/java/src/addressbook/Daemon.java
index a1b1ae18a3..a8a9a2da6e 100644
--- a/apps/addressbook/java/src/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/addressbook/Daemon.java
@@ -28,6 +28,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import net.i2p.I2PAppContext;
+
 /**
  * Main class of addressbook.  Performs updates, and runs the main loop.
  * 
@@ -125,11 +127,13 @@ public class Daemon {
     
     public void run(String[] args) {
         String settingsLocation = "config.txt";
-        String home;
+        File homeFile;
         if (args.length > 0) {
-            home = args[0];
+            homeFile = new File(args[0]);
+            if (!homeFile.isAbsolute())
+                homeFile = new File(I2PAppContext.getGlobalContext().getRouterDir(), args[0]);
         } else {
-            home = ".";
+            homeFile = new File(System.getProperty("user.dir"));
         }
         
         Map defaultSettings = new HashMap();
@@ -145,7 +149,6 @@ public class Daemon {
         defaultSettings.put("last_modified", "last_modified");
         defaultSettings.put("update_delay", "12");
         
-        File homeFile = new File(home);
         if (!homeFile.exists()) {
             boolean created = homeFile.mkdirs();
             if (created)
@@ -169,7 +172,7 @@ public class Daemon {
                 delay = 1;
             }
             
-            update(settings, home);
+            update(settings, homeFile.getAbsolutePath());
             try {
                 synchronized (this) {
                     wait(delay * 60 * 60 * 1000);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
index b7e623060c..1a43514ded 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -73,7 +73,7 @@ public class I2PSnarkUtil {
         // This is used for both announce replies and .torrent file downloads,
         // so it must be available even if not connected to I2CP.
         // so much for multiple instances
-        _tmpDir = new File("tmp", "i2psnark");
+        _tmpDir = new File(ctx.getTempDir(), "i2psnark");
         FileUtil.rmdir(_tmpDir, false);
         _tmpDir.mkdirs();
     }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index a45bf5161a..79ea62ebc4 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -31,7 +31,7 @@ public class SnarkManager implements Snark.CompleteListener {
     /** map of (canonical) filename to Snark instance (unsynchronized) */
     private Map _snarks;
     private Object _addSnarkLock;
-    private String _configFile = "i2psnark.config";
+    private File _configFile;
     private Properties _config;
     private I2PAppContext _context;
     private Log _log;
@@ -51,6 +51,7 @@ public class SnarkManager implements Snark.CompleteListener {
     public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
     public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
 
+    private static final String CONFIG_FILE = "i2psnark.config";
     public static final String PROP_AUTO_START = "i2snark.autoStart";   // oops
     public static final String DEFAULT_AUTO_START = "false";
     public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix";
@@ -66,6 +67,9 @@ public class SnarkManager implements Snark.CompleteListener {
         _log = _context.logManager().getLog(SnarkManager.class);
         _messages = new ArrayList(16);
         _util = new I2PSnarkUtil(_context);
+        _configFile = new File(CONFIG_FILE);
+        if (!_configFile.isAbsolute())
+            _configFile = new File(_context.getConfigDir(), CONFIG_FILE);
         loadConfig(null);
     }
 
@@ -112,10 +116,11 @@ public class SnarkManager implements Snark.CompleteListener {
     }
     private int getStartupDelayMinutes() { return 3; }
     public File getDataDir() { 
-        String dir = _config.getProperty(PROP_DIR);
-        if ( (dir == null) || (dir.trim().length() <= 0) )
-            dir = "i2psnark";
-        return new File(dir); 
+        String dir = _config.getProperty(PROP_DIR, "i2psnark");
+        File f = new File(dir);
+        if (!f.isAbsolute())
+            f = new File(_context.getAppDir(), dir);
+        return f; 
     }
     
     /** null to set initial defaults */
@@ -123,8 +128,10 @@ public class SnarkManager implements Snark.CompleteListener {
         if (_config == null)
             _config = new Properties();
         if (filename != null) {
-            _configFile = filename;
             File cfg = new File(filename);
+            if (!cfg.isAbsolute())
+                cfg = new File(_context.getConfigDir(), filename);
+            _configFile = cfg;
             if (cfg.exists()) {
                 try {
                     DataHelper.loadProps(_config, cfg);
@@ -352,10 +359,10 @@ public class SnarkManager implements Snark.CompleteListener {
     public void saveConfig() {
         try {
             synchronized (_configFile) {
-                DataHelper.storeProps(_config, new File(_configFile));
+                DataHelper.storeProps(_config, _configFile);
             }
         } catch (IOException ioe) {
-            addMessage("Unable to save the config to '" + _configFile + "'");
+            addMessage("Unable to save the config to '" + _configFile.getAbsolutePath() + "'");
         }
     }
     
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 3923484a80..410e804d98 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -173,6 +173,7 @@ public class I2PSnarkServlet extends HttpServlet {
         } else if ("Add torrent".equals(action)) {
             String newFile = req.getParameter("newFile");
             String newURL = req.getParameter("newURL");
+            // NOTE - newFile currently disabled in HTML form - see below
             File f = null;
             if ( (newFile != null) && (newFile.trim().length() > 0) )
                 f = new File(newFile.trim());
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
index 486bcdead5..e4f8453bc7 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
@@ -372,6 +372,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
             }
 
             privKeyFile = new File(args[2]);
+            if (!privKeyFile.isAbsolute())
+                privKeyFile = new File(_context.getAppDir(), args[2]);
             if (!privKeyFile.canRead()) {
                 l.log("private key file does not exist");
                 _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
@@ -419,6 +421,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
             }
 
             privKeyFile = new File(args[2]);
+            if (!privKeyFile.isAbsolute())
+                privKeyFile = new File(_context.getAppDir(), args[2]);
             if (!privKeyFile.canRead()) {
                 l.log("private key file does not exist");
                 _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
@@ -476,6 +480,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
             String spoofedHost = args[2];
             
             privKeyFile = new File(args[3]);
+            if (!privKeyFile.isAbsolute())
+                privKeyFile = new File(_context.getAppDir(), args[3]);
             if (!privKeyFile.canRead()) {
                 l.log("private key file does not exist");
                 _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
@@ -870,6 +876,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
             }
 
             File privKeyFile = new File(args[1]);
+            if (!privKeyFile.isAbsolute())
+                privKeyFile = new File(_context.getAppDir(), args[1]);
             if (!privKeyFile.canRead()) {
                 l.log("private key file does not exist");
                 _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index 59971e8f3d..79270303b0 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -3,6 +3,7 @@
  */
 package net.i2p.i2ptunnel;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -106,6 +107,8 @@ public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runna
     /** used to assign unique IDs to the threads / clients.  no logic or functionality */
     private static volatile long __clientId = 0;
 
+    private static final File _errorDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs");
+
     /**
      * @throws IllegalArgumentException if the I2PTunnel does not contain
      *                                  valid config to contact the router
@@ -261,9 +264,9 @@ public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runna
                 String str;
                 byte[] header;
                 if (usingWWWProxy)
-                    str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
                 else
-                    str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfh-header.ht")).getAbsolutePath(), 100, true);
                 if (str != null)
                     header = str.getBytes();
                 else
@@ -357,9 +360,9 @@ public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runna
             String str;
             byte[] header;
             if (usingWWWProxy)
-                str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
+                str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
             else
-                str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true);
+                str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
             if (str != null)
                 header = str.getBytes();
             else
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index 7e16114adc..cab438991d 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -4,6 +4,7 @@
 package net.i2p.i2ptunnel;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -136,6 +137,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
     /** used to assign unique IDs to the threads / clients.  no logic or functionality */
     private static volatile long __clientId = 0;
 
+    private static final File _errorDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs");
+
+
     /**
      * @throws IllegalArgumentException if the I2PTunnel does not contain
      *                                  valid config to contact the router
@@ -372,7 +376,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
                             {
                                 String str;
                                 byte[] header;
-                                str = FileUtil.readTextFile("docs/ahelper-conflict-header.ht", 100, true);
+                                str = FileUtil.readTextFile((new File(_errorDir, "ahelper-conflict-header.ht")).getAbsolutePath(), 100, true);
                                 if (str != null) header = str.getBytes();
                                   else header = ERR_AHELPER_CONFLICT;
 
@@ -558,13 +562,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
                 byte[] header;
                 boolean showAddrHelper = false;
                 if (usingWWWProxy)
-                    str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
                 else if(ahelper != 0)
-                    str = FileUtil.readTextFile("docs/dnfb-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfb-header.ht")).getAbsolutePath(), 100, true);
                 else if (destination.length() == 60 && destination.endsWith(".b32.i2p"))
-                    str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
                 else {
-                    str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfh-header.ht")).getAbsolutePath(), 100, true);
                     showAddrHelper = true;
                 }
                 if (str != null)
@@ -733,9 +737,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
                 String str;
                 byte[] header;
                 if (usingWWWProxy)
-                    str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
                 else
-                    str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true);
+                    str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
                 if (str != null)
                     header = str.getBytes();
                 else
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 1f67783e9b..3dbcfea306 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -72,6 +72,8 @@ public class TunnelController implements Logging {
         }
         
         File keyFile = new File(getPrivKeyFile());
+        if (!keyFile.isAbsolute())
+            keyFile = new File(I2PAppContext.getGlobalContext().getAppDir(), getPrivKeyFile());
         if (keyFile.exists()) {
             //log("Not overwriting existing private keys in " + keyFile.getAbsolutePath());
             return;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
index 16f418be9e..a0b8ea3f7f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
@@ -270,9 +270,11 @@ public class TunnelControllerGroup {
      */
     private Properties loadConfig(String configFile) {
         File cfgFile = new File(configFile);
+        if (!cfgFile.isAbsolute())
+            cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile);
         if (!cfgFile.exists()) {
             if (_log.shouldLog(Log.ERROR))
-                _log.error("Unable to load the controllers from " + configFile);
+                _log.error("Unable to load the controllers from " + cfgFile.getAbsolutePath());
             return null;
         }
         
@@ -282,7 +284,7 @@ public class TunnelControllerGroup {
             return props;
         } catch (IOException ioe) {
             if (_log.shouldLog(Log.ERROR))
-                _log.error("Error reading the controllers from " + configFile, ioe);
+                _log.error("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe);
             return null;
         }
     }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
index 0dc4d1e62b..195889fad2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
@@ -41,7 +41,7 @@ public class ConfigServiceHandler extends FormHandler {
         }
         public void run() {
             try {
-                Router.killKeys();
+                ContextHelper.getContext(null).router().killKeys();
                 WrapperManager.signalStopped(_exitCode);
             } catch (Throwable t) {
                 t.printStackTrace();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
index ce29250b9f..721655a512 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java
@@ -3,7 +3,6 @@ package net.i2p.router.web;
 import java.io.File;
 import java.util.Locale;
 
-import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 
 public class ContentHelper extends HelperBase {
@@ -14,6 +13,9 @@ public class ContentHelper extends HelperBase {
     
     public ContentHelper() {}
     
+    /**
+     * Caution, use absolute paths only, do not assume files are in CWD
+     */
     public void setPage(String page) { _page = page; }
     public void setStartAtBeginning(String moo) { 
         _startAtBeginning = Boolean.valueOf(""+moo).booleanValue(); 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index e1fce8f3ec..a4896cdb80 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -1,66 +1,70 @@
 package net.i2p.router.web;
 
+import java.io.File;
 import java.util.List;
 
-import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 
 public class LogsHelper extends HelperBase {
     public LogsHelper() {}
     
     public String getLogs() {
-        List msgs = _context.logManager().getBuffer().getMostRecentMessages();
-        StringBuffer buf = new StringBuffer(16*1024); 
-        buf.append("<ul>");
-        buf.append("<code>\n");
-        for (int i = msgs.size(); i > 0; i--) { 
-            String msg = (String)msgs.get(i - 1);
-            msg = msg.replaceAll("<","&lt;");
-            buf.append("<li>");
-            buf.append(msg);
-            buf.append("</li>\n");
-        }
-        buf.append("</code></ul>\n");
-        
-        return buf.toString();
+        return formatMessages(_context.logManager().getBuffer().getMostRecentMessages());
     }
     
     public String getCriticalLogs() {
-        List msgs = _context.logManager().getBuffer().getMostRecentCriticalMessages();
-        StringBuffer buf = new StringBuffer(16*1024); 
-        buf.append("<ul>");
-        buf.append("<code>\n");
-        for (int i = msgs.size(); i > 0; i--) { 
-            String msg = (String)msgs.get(i - 1);
-            msg = msg.replaceAll("<","&lt;");
-            buf.append("<li>");
-            buf.append(msg);
-            buf.append("</li>\n");
-        }
-        buf.append("</code></ul>\n");
-        
-        return buf.toString();
+        return formatMessages(_context.logManager().getBuffer().getMostRecentCriticalMessages());
     }
     
     public String getServiceLogs() {
-        String str = FileUtil.readTextFile("wrapper.log", 250, false);
+        // look in new and old place
+        File f = new File(_context.getLogDir(), "wrapper.log");
+        if (!f.exists())
+            f = new File(_context.getBaseDir(), "wrapper.log");
+        String str = FileUtil.readTextFile(f.getAbsolutePath(), 250, false);
         if (str == null) 
             return "";
         else {
-            str = str.replaceAll("<","&lt;");
+            str = str.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
             return "<pre>" + str + "</pre>";
         }
     }
     
+    /*****  unused
     public String getConnectionLogs() {
-        List msgs = _context.commSystem().getMostRecentErrorMessages();
+        return formatMessages(_context.commSystem().getMostRecentErrorMessages());
+    }
+    ******/
+
+    private String formatMessages(List msgs) {
+        boolean colorize = Boolean.valueOf(_context.getProperty("routerconsole.logs.color")).booleanValue();
         StringBuffer buf = new StringBuffer(16*1024); 
         buf.append("<ul>");
         buf.append("<code>\n");
         for (int i = msgs.size(); i > 0; i--) { 
             String msg = (String)msgs.get(i - 1);
             buf.append("<li>");
-            buf.append(msg);
+            if (colorize) {
+                String color;
+                // Homeland Security Advisory System
+                // http://www.dhs.gov/xinfoshare/programs/Copy_of_press_release_0046.shtm
+                // but pink instead of yellow for WARN
+                if (msg.contains("CRIT"))
+                    color = "#cc0000";
+                else if (msg.contains("ERROR"))
+                    color = "#ff3300";
+                else if (msg.contains("WARN"))
+                    color = "#ff00cc";
+                else if (msg.contains("INFO"))
+                    color = "#000099";
+                else
+                    color = "#006600";
+                buf.append("<font color=\"").append(color).append("\">");
+                buf.append(msg.replaceAll("<", "&lt;").replaceAll(">", "&gt;"));
+                buf.append("</font>");
+            } else {
+                buf.append(msg);
+            }
             buf.append("</li>\n");
         }
         buf.append("</code></ul>\n");
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
index 98fac325d1..fd7662caf4 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java
@@ -27,6 +27,8 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
     private long _lastUpdated;
     private String _updateVersion;
     private String _lastModified;
+    private File _newsFile;
+    private File _tempFile;
     private static NewsFetcher _instance;
     //public static final synchronized NewsFetcher getInstance() { return _instance; }
     public static final synchronized NewsFetcher getInstance(I2PAppContext ctx) { 
@@ -35,25 +37,26 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
         _instance = new NewsFetcher(ctx);
         return _instance;
     }
-    
+
     private static final String NEWS_FILE = "docs/news.xml";
-    private static final String TEMP_NEWS_FILE = "docs/news.xml.temp";
+    private static final String TEMP_NEWS_FILE = "news.xml.temp";
     
     private NewsFetcher(I2PAppContext ctx) {
         _context = ctx;
         _log = ctx.logManager().getLog(NewsFetcher.class);
         _instance = this;
         _lastFetch = 0;
+        _newsFile = new File(_context.getBaseDir(), NEWS_FILE);
+        _tempFile = new File(_context.getTempDir(), TEMP_NEWS_FILE);
         updateLastFetched();
         _lastUpdated = _lastFetch;
         _updateVersion = "";
     }
     
     private void updateLastFetched() {
-        File news = new File(NEWS_FILE);
-        if (news.exists()) {
+        if (_newsFile.exists()) {
             if (_lastFetch == 0)
-                _lastFetch = news.lastModified();
+                _lastFetch = _newsFile.lastModified();
         } else
             _lastFetch = 0;
     }
@@ -82,7 +85,7 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
         String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
         if ("notify".equals(policy))
             return false;
-        File zip = new File(Router.UPDATE_FILE);
+        File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
         return !zip.exists();
     }
     
@@ -114,18 +117,17 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
         boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
         String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
         String port = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT);
-        File tempFile = new File(TEMP_NEWS_FILE);
-        if (tempFile.exists())
-            tempFile.delete();
+        if (_tempFile.exists())
+            _tempFile.delete();
         
         int proxyPort = -1;
         try {
             proxyPort = Integer.parseInt(port);
             EepGet get = null;
             if (shouldProxy)
-                get = new EepGet(_context, true, proxyHost, proxyPort, 2, TEMP_NEWS_FILE, newsURL, true, null, _lastModified);
+                get = new EepGet(_context, true, proxyHost, proxyPort, 2, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
             else
-                get = new EepGet(_context, false, null, 0, 0, TEMP_NEWS_FILE, newsURL, true, null, _lastModified);
+                get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
             get.addStatusListener(this);
             if (get.fetch())
                 _lastModified = get.getLastModified();
@@ -138,11 +140,10 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
     private static final String VERSION_PREFIX = "version=\"";
     private void checkForUpdates() {
         _updateAvailable = false;
-        File news = new File(NEWS_FILE);
-        if ( (!news.exists()) || (news.length() <= 0) ) return;
+        if ( (!_newsFile.exists()) || (_newsFile.length() <= 0) ) return;
         FileInputStream in = null;
         try {
-            in = new FileInputStream(news);
+            in = new FileInputStream(_newsFile);
             StringBuffer buf = new StringBuffer(128);
             while (DataHelper.readLine(in, buf)) {
                 int index = buf.indexOf(VERSION_PREFIX);
@@ -220,13 +221,12 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
         if (_log.shouldLog(Log.INFO))
             _log.info("News fetched from " + url + " with " + (alreadyTransferred+bytesTransferred));
         
-        File temp = new File(TEMP_NEWS_FILE);
         long now = _context.clock().now();
-        if (temp.exists()) {
-            boolean copied = FileUtil.copy(TEMP_NEWS_FILE, NEWS_FILE, true);
+        if (_tempFile.exists()) {
+            boolean copied = FileUtil.copy(_tempFile.getAbsolutePath(), _newsFile.getAbsolutePath(), true);
             if (copied) {
                 _lastUpdated = now;
-                temp.delete();
+                _tempFile.delete();
                 checkForUpdates();
             } else {
                 if (_log.shouldLog(Log.ERROR))
@@ -242,8 +242,7 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener {
     public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
         if (_log.shouldLog(Log.WARN))
             _log.warn("Failed to fetch the news from " + url);
-        File temp = new File(TEMP_NEWS_FILE);
-        temp.delete();
+        _tempFile.delete();
     }
     public void headerReceived(String url, int attemptNum, String key, String val) {}
     public void attempting(String url) {}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ReseedHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ReseedHandler.java
index e701ede995..c11c6150c2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ReseedHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ReseedHandler.java
@@ -256,7 +256,7 @@ public class ReseedHandler {
     
         private void writeSeed(String name, byte data[]) throws Exception {
             String dirName = "netDb"; // _context.getProperty("router.networkDatabase.dbDir", "netDb");
-            File netDbDir = new File(dirName);
+            File netDbDir = new File(_context.getRouterDir(), dirName);
             if (!netDbDir.exists()) {
                 boolean ok = netDbDir.mkdirs();
             }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index 862902b1f8..f58aeb3082 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -59,7 +59,7 @@ public class RouterConsoleRunner {
     }
     
     public void startConsole() {
-        File workDir = new File("work");
+        File workDir = new File(I2PAppContext.getGlobalContext().getTempDir(), "jetty-work");
         boolean workDirRemoved = FileUtil.rmdir(workDir, false);
         if (!workDirRemoved)
             System.err.println("ERROR: Unable to remove Jetty temporary work directory");
@@ -95,8 +95,11 @@ public class RouterConsoleRunner {
             }
             _server.setRootWebApp(ROUTERCONSOLE);
             WebApplicationContext wac = _server.addWebApplication("/", _webAppsDir + ROUTERCONSOLE + ".war");
+            File tmpdir = new File(workDir, ROUTERCONSOLE + "-" + _listenPort);
+            tmpdir.mkdir();
+            wac.setTempDirectory(tmpdir);
             initialize(wac);
-            File dir = new File(_webAppsDir);
+            File dir = new File(I2PAppContext.getGlobalContext().getBaseDir(), _webAppsDir);
             String fileNames[] = dir.list(WarFilenameFilter.instance());
             if (fileNames != null) {
                 for (int i = 0; i < fileNames.length; i++) {
@@ -106,6 +109,9 @@ public class RouterConsoleRunner {
                         if (! "false".equals(enabled)) {
                             String path = new File(dir, fileNames[i]).getCanonicalPath();
                             wac = _server.addWebApplication("/"+ appName, path);
+                            tmpdir = new File(workDir, appName + "-" + _listenPort);
+                            tmpdir.mkdir();
+                            wac.setTempDirectory(tmpdir);
                             initialize(wac);
                             if (enabled == null) {
                                 // do this so configclients.jsp knows about all apps from reading the config
@@ -144,10 +150,10 @@ public class RouterConsoleRunner {
         // don't have an installation directory that they can put the flag in yet.
         File noReseedFile = new File(new File(System.getProperty("user.home")), ".i2pnoreseed");
         File noReseedFileAlt1 = new File(new File(System.getProperty("user.home")), "noreseed.i2p");
-        File noReseedFileAlt2 = new File(".i2pnoreseed");
-        File noReseedFileAlt3 = new File("noreseed.i2p");
+        File noReseedFileAlt2 = new File(I2PAppContext.getGlobalContext().getConfigDir(), ".i2pnoreseed");
+        File noReseedFileAlt3 = new File(I2PAppContext.getGlobalContext().getConfigDir(), "noreseed.i2p");
         if (!noReseedFile.exists() && !noReseedFileAlt1.exists() && !noReseedFileAlt2.exists() && !noReseedFileAlt3.exists()) {
-            File netDb = new File("netDb");
+            File netDb = new File(I2PAppContext.getGlobalContext().getRouterDir(), "netDb");
             // sure, some of them could be "my.info" or various leaseSet- files, but chances are, 
             // if someone has those files, they've already been seeded (at least enough to let them
             // get i2p started - they can reseed later in the web console)
@@ -216,7 +222,7 @@ public class RouterConsoleRunner {
         Properties rv = new Properties();
         // String webappConfigFile = ctx.getProperty(PROP_WEBAPP_CONFIG_FILENAME, DEFAULT_WEBAPP_CONFIG_FILENAME);
         String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
-        File cfgFile = new File(webappConfigFile);
+        File cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), webappConfigFile);
         
         try {
             DataHelper.loadProps(rv, cfgFile);
@@ -230,7 +236,7 @@ public class RouterConsoleRunner {
     public static void storeWebAppProperties(Properties props) {
         // String webappConfigFile = ctx.getProperty(PROP_WEBAPP_CONFIG_FILENAME, DEFAULT_WEBAPP_CONFIG_FILENAME);
         String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
-        File cfgFile = new File(webappConfigFile);
+        File cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), webappConfigFile);
         
         try {
             DataHelper.storeProps(props, cfgFile);
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
index 35edadfb6b..267fe5a07c 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
@@ -31,6 +31,7 @@ public class UpdateHandler {
     protected RouterContext _context;
     protected Log _log;
     protected DecimalFormat _pct = new DecimalFormat("00.0%");
+    protected String _updateFile;
     
     protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
     protected static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress";
@@ -41,6 +42,7 @@ public class UpdateHandler {
     public UpdateHandler(RouterContext ctx) {
         _context = ctx;
         _log = ctx.logManager().getLog(UpdateHandler.class);
+        _updateFile = (new File(ctx.getRouterDir(), SIGNED_UPDATE_FILE)).getAbsolutePath();
     }
     
     /**
@@ -137,9 +139,9 @@ public class UpdateHandler {
             try {
                 EepGet get = null;
                 if (shouldProxy)
-                    get = new EepGet(_context, proxyHost, proxyPort, 20, SIGNED_UPDATE_FILE, updateURL, false);
+                    get = new EepGet(_context, proxyHost, proxyPort, 20, _updateFile, updateURL, false);
                 else
-                    get = new EepGet(_context, 1, SIGNED_UPDATE_FILE, updateURL, false);
+                    get = new EepGet(_context, 1, _updateFile, updateURL, false);
                 get.addStatusListener(UpdateRunner.this);
                 get.fetch();
             } catch (Throwable t) {
@@ -167,8 +169,9 @@ public class UpdateHandler {
         public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
             _status = "<b>Update downloaded</b>";
             TrustedUpdate up = new TrustedUpdate(_context);
-            String err = up.migrateVerified(RouterVersion.VERSION, SIGNED_UPDATE_FILE, Router.UPDATE_FILE);
-            File f = new File(SIGNED_UPDATE_FILE);
+            File f = new File(_updateFile);
+            File to = new File(_context.getBaseDir(), Router.UPDATE_FILE);
+            String err = up.migrateVerified(RouterVersion.VERSION, f, to);
             f.delete();
             if (err == null) {
                 String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
diff --git a/apps/routerconsole/jsp/flags.jsp b/apps/routerconsole/jsp/flags.jsp
index 38d068d592..c990ba27a3 100644
--- a/apps/routerconsole/jsp/flags.jsp
+++ b/apps/routerconsole/jsp/flags.jsp
@@ -15,8 +15,11 @@ if (c != null && c.length() > 0) {
     java.io.OutputStream cout = response.getOutputStream();
     response.setContentType("image/png");
     response.setHeader("Cache-Control", "max-age=86400");  // cache for a day
+    String base = net.i2p.I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath();
+    String file = "docs" + java.io.File.separatorChar + "icons" + java.io.File.separatorChar +
+                  "flags" + java.io.File.separatorChar + c + ".png";
     try {
-        net.i2p.util.FileUtil.readFile(c + ".png", "docs/icons/flags", cout);
+        net.i2p.util.FileUtil.readFile(file, base, cout);
         rendered = true;
     } catch (java.io.IOException ioe) {}
     if (rendered)
diff --git a/apps/routerconsole/jsp/viewtheme.jsp b/apps/routerconsole/jsp/viewtheme.jsp
index 05ccfecbf9..5785195c29 100644
--- a/apps/routerconsole/jsp/viewtheme.jsp
+++ b/apps/routerconsole/jsp/viewtheme.jsp
@@ -1,4 +1,4 @@
-<% 
+% 
 /*
  * USE CAUTION WHEN EDITING
  * Trailing whitespace OR NEWLINE on the last line will cause
@@ -16,5 +16,7 @@ if (uri.endsWith(".css")) {
   response.setContentType("image/jpeg");
 }
 
-net.i2p.util.FileUtil.readFile(uri, "./docs", response.getOutputStream());
+String base = net.i2p.I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath() +
+              java.io.File.separatorChar + "docs";
+net.i2p.util.FileUtil.readFile(uri, base, response.getOutputStream());
 %>
\ No newline at end of file
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java b/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
index 15a2204f8e..8cddafdc42 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
@@ -33,12 +33,16 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.Serializable;
 
+import net.i2p.I2PAppContext;
+
 public class ConfigBean implements Serializable {
 	
 	/*
 	 * as this is not provided as constant in addressbook, we define it here
 	 */
-	public static final String addressbookPrefix = "addressbook/";
+ 	public static final String addressbookPrefix =
+ 		(new File(I2PAppContext.getGlobalContext().getRouterDir(), "addressbook")).getAbsolutePath()
+			+ File.separatorChar;
 	public static final String configFileName = addressbookPrefix + "config.txt";
 	
 	private String action, config;
diff --git a/apps/susimail/build.xml b/apps/susimail/build.xml
index 886f9471f6..fb43716144 100644
--- a/apps/susimail/build.xml
+++ b/apps/susimail/build.xml
@@ -15,6 +15,7 @@
             <classpath>
                 <pathelement location="../jetty/jettylib/javax.servlet.jar" />
                 <pathelement location="../jetty/jettylib/org.mortbay.jetty.jar" />
+                <pathelement location="../../core/java/build/i2p.jar" />
             </classpath>
         </javac>
     </target>
diff --git a/apps/susimail/src/src/i2p/susi/util/Config.java b/apps/susimail/src/src/i2p/susi/util/Config.java
index 219ce71f34..723c56b992 100644
--- a/apps/susimail/src/src/i2p/susi/util/Config.java
+++ b/apps/susimail/src/src/i2p/susi/util/Config.java
@@ -25,10 +25,13 @@ package i2p.susi.util;
 
 import i2p.susi.debug.Debug;
 
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.Properties;
 
+import net.i2p.I2PAppContext;
+
 /**
  * @author susi
  */
@@ -81,7 +84,8 @@ public class Config {
 		}
                 FileInputStream fis = null;
 		try {
-			fis = new FileInputStream( "susimail.config" );
+			File cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), "susimail.config");
+			fis = new FileInputStream(cfg);
 			config.load( fis );
 		} catch (Exception e) {
 			Debug.debug( Debug.DEBUG, "Could not open susimail.config, reason: " + e.getMessage() );
diff --git a/core/java/src/freenet/support/CPUInformation/CPUID.java b/core/java/src/freenet/support/CPUInformation/CPUID.java
index 1d5c848828..58469c6d74 100644
--- a/core/java/src/freenet/support/CPUInformation/CPUID.java
+++ b/core/java/src/freenet/support/CPUInformation/CPUID.java
@@ -9,6 +9,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
 
+import net.i2p.I2PAppContext;
+
 /**
  * @author Iakin
  * A class for retrieveing details about the CPU using the CPUID assembly instruction.
@@ -503,9 +505,10 @@ public class CPUID {
         FileOutputStream fos = null;
         try {
             InputStream libStream = resource.openStream();
-            outFile = new File(libPrefix + "jcpuid" + libSuffix);
+            outFile = new File(I2PAppContext.getGlobalContext().getBaseDir(), libPrefix + "jcpuid" + libSuffix);
             fos = new FileOutputStream(outFile);
-            byte buf[] = new byte[4096*1024];
+            // wtf this was 4096*1024 which is really excessive for a roughly 4KB file
+            byte buf[] = new byte[4096];
             while (true) {
                 int read = libStream.read(buf);
                 if (read < 0) break;
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 3f44146b43..56f774071c 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -1,5 +1,6 @@
 package net.i2p;
 
+import java.io.File;
 import java.util.HashSet;
 import java.util.Properties;
 import java.util.Set;
@@ -20,10 +21,12 @@ import net.i2p.crypto.KeyGenerator;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.crypto.TransientSessionKeyManager;
+                                            import net.i2p.data.DataHelper;
 import net.i2p.data.RoutingKeyGenerator;
 import net.i2p.stat.StatManager;
 import net.i2p.util.Clock;
 import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.FileUtil;
 import net.i2p.util.FortunaRandomSource;
 import net.i2p.util.KeyRing;
 import net.i2p.util.LogManager;
@@ -96,6 +99,13 @@ public class I2PAppContext {
     private volatile boolean _keyGeneratorInitialized;
     protected volatile boolean _keyRingInitialized; // used in RouterContext
     private Set<Runnable> _shutdownTasks;
+    private File _baseDir;
+    private File _configDir;
+    private File _routerDir;
+    private File _pidDir;
+    private File _logDir;
+    private File _appDir;
+    private File _tmpDir;
     
     
     /**
@@ -155,8 +165,148 @@ public class I2PAppContext {
         _logManagerInitialized = false;
         _keyRingInitialized = false;
         _shutdownTasks = new ConcurrentHashSet(0);
+        initializeDirs();
     }
     
+   /**
+    *  Directories. These are all set at instantiation and will not be changed by
+    *  subsequent property changes.
+    *  All properties, if set, should be absolute paths.
+    *
+    *  Name	Property 	Method		Files
+    *  -----	-------- 	-----		-----
+    *  Base	i2p.dir.base	getBaseDir()	lib/, webapps/, docs/, geoip/, licenses/, ...
+    *  Temp	i2p.dir.temp	getTempDir()	Temporary files
+    *  Config	i2p.dir.config	getConfigDir()	*.config, hosts.txt, addressbook/, ...
+    *
+    *  (the following all default to the same as Config)
+    *
+    *  Router	i2p.dir.router	getRouterDir()	netDb/, peerProfiles/, router.*, keyBackup/, ...
+    *  Log	i2p.dir.log	getLogDir()	wrapper.log*, logs/
+    *  PID	i2p.dir.pid	getPIDDir()	wrapper *.pid files, router.ping
+    *  App	i2p.dir.app	getAppDir()	eepsite/, ...
+    *
+    *  Note that we can't control where the wrapper puts its files.
+    *
+    *  The app dir is where all data files should be. Apps should always read and write files here,
+    *  using a constructor such as:
+    *
+    *       String path = mypath;
+    *       File f = new File(path);
+    *       if (!f.isAbsolute())
+    *           f = new File(_context.geAppDir(), path);
+    *
+    *  and never attempt to access files in the CWD using
+    *
+    *       File f = new File("foo");
+    *
+    *  An app should assume the CWD is not writable.
+    *
+    *  Here in I2PAppContext, all the dirs default to CWD.
+    *  However these will be different in RouterContext, as Router.java will set
+    *  the properties in the RouterContext constructor.
+    *
+    *  Apps should never need to access the base dir, which is the location of the base I2P install.
+    *  However this is provided for the router's use, and for backward compatibility should an app
+    *  need to look there as well.
+    *
+    *  All dirs except the base are created if they don't exist, but the creation will fail silently.
+    */
+    private void initializeDirs() {
+        String s = getProperty("i2p.dir.base", System.getProperty("user.dir"));
+        _baseDir = new File(s);
+        // config defaults to base
+        s = getProperty("i2p.dir.config");
+        if (s != null) {
+            _configDir = new File(s);
+            if (!_configDir.exists())
+                _configDir.mkdir();
+        } else {
+            _configDir = _baseDir;
+        }
+        _configDir = new File(s);
+        // router defaults to config
+        s = getProperty("i2p.dir.router");
+        if (s != null) {
+            _routerDir = new File(s);
+            if (!_routerDir.exists())
+                _routerDir.mkdir();
+        } else {
+            _routerDir = _configDir;
+        }
+        // these all default to router
+        s = getProperty("i2p.dir.pid");
+        if (s != null) {
+            _pidDir = new File(s);
+            if (!_pidDir.exists())
+                _pidDir.mkdir();
+        } else {
+            _pidDir = _routerDir;
+        }
+        s = getProperty("i2p.dir.log");
+        if (s != null) {
+            _logDir = new File(s);
+            if (!_logDir.exists())
+                _logDir.mkdir();
+        } else {
+            _logDir = _routerDir;
+        }
+        s = getProperty("i2p.dir.app");
+        if (s != null) {
+            _appDir = new File(s);
+            if (!_appDir.exists())
+                _appDir.mkdir();
+        } else {
+            _appDir = _routerDir;
+        }
+        // comment these out later, don't want user names in the wrapper logs
+        System.err.println("Base directory:   " + _baseDir.getAbsolutePath());
+        System.err.println("Config directory: " + _configDir.getAbsolutePath());
+        System.err.println("Router directory: " + _routerDir.getAbsolutePath());
+        System.err.println("App directory:    " + _appDir.getAbsolutePath());
+        System.err.println("Log directory:    " + _logDir.getAbsolutePath());
+        System.err.println("PID directory:    " + _pidDir.getAbsolutePath());
+        System.err.println("Temp directory:   " + getTempDir().getAbsolutePath());
+    }
+
+    public File getBaseDir() { return _baseDir; }
+    public File getConfigDir() { return _configDir; }
+    public File getRouterDir() { return _routerDir; }
+    public File getPIDDir() { return _pidDir; }
+    public File getLogDir() { return _logDir; }
+    public File getAppDir() { return _appDir; }
+    public File getTempDir() {
+        // fixme don't synchronize every time
+        synchronized (this) {
+            if (_tmpDir == null) {
+                String d = getProperty("i2p.dir.temp", System.getProperty("java.io.tmpdir"));
+                // our random() probably isn't warmed up yet
+                String f = "i2p-" + (new java.util.Random()).nextInt() + ".tmp";
+                _tmpDir = new File(d, f);
+                if (_tmpDir.exists()) {
+                    // good or bad ?
+                } else if (_tmpDir.mkdir()) {
+                    _tmpDir.deleteOnExit();
+                } else {
+                    System.err.println("Could not create temp dir " + _tmpDir.getAbsolutePath());
+                    _tmpDir = new File(_routerDir, "tmp");
+                    _tmpDir.mkdir();
+                }
+            }
+        }
+        return _tmpDir;
+    }
+
+    /** don't rely on deleteOnExit() */
+    public void deleteTempDir() {
+        synchronized (this) {
+            if (_tmpDir != null) {
+                FileUtil.rmdir(_tmpDir, false);
+                _tmpDir = null;
+            }
+        }
+    }
+
     /**
      * Access the configuration attributes of this context, using properties 
      * provided during the context construction, or falling back on 
diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
index 054bd9d8f1..f89d56d09d 100644
--- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
+++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
@@ -89,7 +89,7 @@ public class HostsTxtNamingService extends NamingService {
             String hostsfile = (String)filenames.get(i);
             Properties hosts = new Properties();
             try {
-                File f = new File(hostsfile);
+                File f = new File(_context.getRouterDir(), hostsfile);
                 if ( (f.exists()) && (f.canRead()) ) {
                     DataHelper.loadProps(hosts, f, true);
                     
@@ -119,7 +119,7 @@ public class HostsTxtNamingService extends NamingService {
             String hostsfile = (String)filenames.get(i);
             Properties hosts = new Properties();
             try {
-                File f = new File(hostsfile);
+                File f = new File(_context.getRouterDir(), hostsfile);
                 if ( (f.exists()) && (f.canRead()) ) {
                     DataHelper.loadProps(hosts, f, true);
                     Set keyset = hosts.keySet();
@@ -145,7 +145,7 @@ public class HostsTxtNamingService extends NamingService {
             String hostsfile = (String)filenames.get(i);
             Properties hosts = new Properties();
             try {
-                File f = new File(hostsfile);
+                File f = new File(_context.getRouterDir(), hostsfile);
                 if ( (f.exists()) && (f.canRead()) ) {
                     DataHelper.loadProps(hosts, f, true);
                     Set keyset = hosts.keySet();
diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java
index 06e37544b3..6130fa78d5 100644
--- a/core/java/src/net/i2p/crypto/TrustedUpdate.java
+++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java
@@ -1,6 +1,7 @@
 package net.i2p.crypto;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -276,7 +277,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
     }
 
     private static final void showVersionCLI(String signedFile) {
-        String versionString = new TrustedUpdate().getVersionString(signedFile);
+        String versionString = new TrustedUpdate().getVersionString(new File(signedFile));
 
         if (versionString == "")
             System.out.println("No version string found in file '" + signedFile + "'");
@@ -294,7 +295,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
     }
 
     private static final void verifySigCLI(String signedFile) {
-        boolean isValidSignature = new TrustedUpdate().verify(signedFile);
+        boolean isValidSignature = new TrustedUpdate().verify(new File(signedFile));
 
         if (isValidSignature)
             System.out.println("Signature VALID");
@@ -303,7 +304,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
     }
 
     private static final void verifyUpdateCLI(String signedFile) {
-        boolean isUpdate = new TrustedUpdate().isUpdatedVersion(CoreVersion.VERSION, signedFile);
+        boolean isUpdate = new TrustedUpdate().isUpdatedVersion(CoreVersion.VERSION, new File(signedFile));
 
         if (isUpdate)
             System.out.println("File version is newer than current version.");
@@ -347,7 +348,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return The version string read, or an empty string if no version string
      *         is present.
      */
-    public String getVersionString(String signedFile) {
+    public String getVersionString(File signedFile) {
         FileInputStream fileInputStream = null;
 
         try {
@@ -396,9 +397,9 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return <code>true</code> if the signed update file's version is newer
      *         than the current version, otherwise <code>false</code>.
      */
-    public boolean isUpdatedVersion(String currentVersion, String signedFile) {
+    public boolean isUpdatedVersion(String currentVersion, File signedFile) {
         _newVersion = getVersionString(signedFile);
-        return needsUpdate(currentVersion, getVersionString(signedFile));
+        return needsUpdate(currentVersion, _newVersion);
     }
 
     /**
@@ -413,7 +414,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return <code>null</code> if the signature and version were valid and the
      *         data was moved, and an error <code>String</code> otherwise.
      */
-    public String migrateVerified(String currentVersion, String signedFile, String outputFile) {
+    public String migrateVerified(String currentVersion, File signedFile, File outputFile) {
         if (!isUpdatedVersion(currentVersion, signedFile))
             return "Downloaded version is not greater than current version";
 
@@ -606,7 +607,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return <code>true</code> if the file has a valid signature, otherwise
      *         <code>false</code>.
      */
-    public boolean verify(String signedFile) {
+    public boolean verify(File signedFile) {
         for (int i = 0; i < _trustedKeys.size(); i++) {
             SigningPublicKey signingPublicKey = new SigningPublicKey();
 
@@ -662,7 +663,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
                 }
         }
 
-        return verify(signedFile, signingPublicKey);
+        return verify(new File(signedFile), signingPublicKey);
     }
 
     /**
@@ -676,7 +677,7 @@ D8usM7Dxp5yrDrCYZ5AIijc=
      * @return <code>true</code> if the file has a valid signature, otherwise
      *         <code>false</code>.
      */
-    public boolean verify(String signedFile, SigningPublicKey signingPublicKey) {
+    public boolean verify(File signedFile, SigningPublicKey signingPublicKey) {
         FileInputStream fileInputStream = null;
 
         try {
diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java
index b5d68ee411..497733cc4e 100644
--- a/core/java/src/net/i2p/data/PrivateKeyFile.java
+++ b/core/java/src/net/i2p/data/PrivateKeyFile.java
@@ -109,6 +109,10 @@ public class PrivateKeyFile {
         this(new File(file), I2PClientFactory.createClient());
     }
 
+    public PrivateKeyFile(File file) {
+        this(file, I2PClientFactory.createClient());
+    }
+
     public PrivateKeyFile(File file, I2PClient client) {
         this.file = file;
         this.client = client;
diff --git a/core/java/src/net/i2p/stat/BufferedStatLog.java b/core/java/src/net/i2p/stat/BufferedStatLog.java
index 7fed2d0904..ca016622fa 100644
--- a/core/java/src/net/i2p/stat/BufferedStatLog.java
+++ b/core/java/src/net/i2p/stat/BufferedStatLog.java
@@ -1,6 +1,7 @@
 package net.i2p.stat;
 
 import java.io.BufferedWriter;
+import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
@@ -109,6 +110,9 @@ public class BufferedStatLog implements StatLog {
         String filename = _context.getProperty(StatManager.PROP_STAT_FILE);
         if (filename == null)
             filename = StatManager.DEFAULT_STAT_FILE;
+        File foo = new File(filename);
+        if (!foo.isAbsolute())
+            filename = (new File(_context.getRouterDir(), filename)).getAbsolutePath();
         if ( (_outFile != null) && (_outFile.equals(filename)) ) {
             // noop
         } else {
diff --git a/core/java/src/net/i2p/util/FileUtil.java b/core/java/src/net/i2p/util/FileUtil.java
index 677c500866..05346ccfe2 100644
--- a/core/java/src/net/i2p/util/FileUtil.java
+++ b/core/java/src/net/i2p/util/FileUtil.java
@@ -18,6 +18,12 @@ import java.util.zip.ZipFile;
 /**
  * General helper methods for messing with files
  *
+ * These are static methods that do NOT convert arguments
+ * to absolute paths for a particular context and directtory.
+ *
+ * Callers should ALWAYS provide absolute paths as arguments,
+ * and should NEVER assume files are in the current working directory.
+ *
  */
 public class FileUtil {
     /**
diff --git a/core/java/src/net/i2p/util/LogManager.java b/core/java/src/net/i2p/util/LogManager.java
index 1f957515ca..c07445d9b5 100644
--- a/core/java/src/net/i2p/util/LogManager.java
+++ b/core/java/src/net/i2p/util/LogManager.java
@@ -67,8 +67,8 @@ public class LogManager {
     /** when was the config file last read (or -1 if never) */
     private long _configLastRead;
 
-    /** filename of the config file */
-    private String _location;
+    /** the config file */
+    private File _locationFile;
     /** Ordered list of LogRecord elements that have not been written out yet */
     private List _records;
     /** List of explicit overrides of log levels (LogLimit objects) */
@@ -115,11 +115,11 @@ public class LogManager {
         _logs = new HashMap(128);
         _defaultLimit = Log.ERROR;
         _configLastRead = 0;
-        _location = context.getProperty(CONFIG_LOCATION_PROP, CONFIG_LOCATION_DEFAULT);
         _context = context;
         _log = getLog(LogManager.class);
+        String location = context.getProperty(CONFIG_LOCATION_PROP, CONFIG_LOCATION_DEFAULT);
+        setConfig(location);
         _consoleBuffer = new LogConsoleBuffer(context);
-        loadConfig();
         _writer = new LogWriter(this);
         Thread t = new I2PThread(_writer);
         t.setName("LogWriter");
@@ -196,8 +196,9 @@ public class LogManager {
     }
 
     public void setConfig(String filename) {
-        _log.debug("Config filename set to " + filename);
-        _location = filename;
+        _locationFile = new File(filename);
+        if (!_locationFile.isAbsolute())
+            _locationFile = new File(_context.getConfigDir(), filename);
         loadConfig();
     }
 
@@ -232,20 +233,12 @@ public class LogManager {
         loadConfig();
     }
 
-    ///
-    ///
-
-    //
-    //
-    //
-
     private void loadConfig() {
-        File cfgFile = new File(_location);
+        File cfgFile = _locationFile;
         if (!cfgFile.exists()) {
             if (!_alreadyNoticedMissingConfig) {
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("Log file " + _location + " does not exist");
-                //System.err.println("Log file " + _location + " does not exist");
+                    _log.warn("Log file " + _locationFile.getAbsolutePath() + " does not exist");
                 _alreadyNoticedMissingConfig = true;
             }
             parseConfig(new Properties());
@@ -262,9 +255,6 @@ public class LogManager {
             return;
         }
 
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Loading config from " + _location);
-
         Properties p = new Properties();
         FileInputStream fis = null;
         try {
@@ -272,7 +262,7 @@ public class LogManager {
             p.load(fis);
             _configLastRead = _context.clock().now();
         } catch (IOException ioe) {
-            System.err.println("Error loading logger config from " + new File(_location).getAbsolutePath());
+            System.err.println("Error loading logger config from " + cfgFile.getAbsolutePath());
         } finally {
             if (fis != null) try {
                 fis.close();
@@ -540,7 +530,7 @@ public class LogManager {
         String config = createConfig();
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(_location);
+            fos = new FileOutputStream(_locationFile);
             fos.write(config.getBytes());
             return true;
         } catch (IOException ioe) {
diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java
index 29fcff7ccd..c9f2cb7562 100644
--- a/core/java/src/net/i2p/util/LogWriter.java
+++ b/core/java/src/net/i2p/util/LogWriter.java
@@ -15,6 +15,8 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
 
+import net.i2p.I2PAppContext;
+
 /**
  * Log writer thread that pulls log records from the LogManager, writes them to
  * the current logfile, and rotates the logs as necessary.  This also periodically
@@ -22,6 +24,7 @@ import java.util.List;
  *
  */
 class LogWriter implements Runnable {
+    /** every 10 seconds? why? Just have the gui force a reread after a change?? */
     private final static long CONFIG_READ_ITERVAL = 10 * 1000;
     private long _lastReadConfig = 0;
     private long _numBytesInCurrentFile = 0;
@@ -38,6 +41,7 @@ class LogWriter implements Runnable {
 
     public LogWriter(LogManager manager) {
         _manager = manager;
+        _lastReadConfig = Clock.getInstance().now();
     }
 
     public void stopWriting() {
@@ -168,15 +172,21 @@ class LogWriter implements Runnable {
      *
      */
     private File getNextFile(String pattern) {
-        File f = null;
+        File f = new File(pattern);
+        File base = null;
+        if (!f.isAbsolute())
+            base = I2PAppContext.getGlobalContext().getLogDir();
 
         if ( (pattern.indexOf('#') < 0) && (pattern.indexOf('@') <= 0) ) {
-            return new File(pattern);
+            if (base != null)
+                return new File(base, pattern);
+            else
+                return f;
         }
         
         int max = _manager.getRotationLimit();
         if (_rotationNum == -1) {
-            return getFirstFile(pattern, max);
+            return getFirstFile(base, pattern, max);
         }
              
         // we're in rotation, just go to the next  
@@ -190,9 +200,13 @@ class LogWriter implements Runnable {
      * Retrieve the first file, updating the rotation number accordingly
      *
      */
-    private File getFirstFile(String pattern, int max) {
+    private File getFirstFile(File base, String pattern, int max) {
         for (int i = 0; i < max; i++) {
-            File f = new File(replace(pattern, i));
+            File f;
+            if (base != null)
+                f = new File(base, replace(pattern, i));
+            else
+                f = new File(replace(pattern, i));
             if (!f.exists()) {
                 _rotationNum = i;
                 return f;
@@ -202,7 +216,11 @@ class LogWriter implements Runnable {
         // all exist, pick the oldest to replace
         File oldest = null;
         for (int i = 0; i < max; i++) {
-            File f = new File(replace(pattern, i));
+            File f;
+            if (base != null)
+                f = new File(base, replace(pattern, i));
+            else
+                f = new File(replace(pattern, i));
             if (oldest == null) {
                 oldest = f;
             } else {
diff --git a/core/java/src/net/i2p/util/NativeBigInteger.java b/core/java/src/net/i2p/util/NativeBigInteger.java
index a6d7e97925..bafaa5e5c9 100644
--- a/core/java/src/net/i2p/util/NativeBigInteger.java
+++ b/core/java/src/net/i2p/util/NativeBigInteger.java
@@ -540,9 +540,10 @@ public class NativeBigInteger extends BigInteger {
         FileOutputStream fos = null;
         try {
             InputStream libStream = resource.openStream();
-            outFile = new File(_libPrefix + "jbigi" + _libSuffix);
+            outFile = new File(I2PAppContext.getGlobalContext().getBaseDir(), _libPrefix + "jbigi" + _libSuffix);
             fos = new FileOutputStream(outFile);
-            byte buf[] = new byte[4096*1024];
+            // wtf this was 4096*1024 which is really excessive for a roughly 50KB file
+            byte buf[] = new byte[4096];
             while (true) {
                 int read = libStream.read(buf);
                 if (read < 0) break;
diff --git a/core/java/src/net/i2p/util/RandomSource.java b/core/java/src/net/i2p/util/RandomSource.java
index 550b37b8d2..c6ac80a65c 100644
--- a/core/java/src/net/i2p/util/RandomSource.java
+++ b/core/java/src/net/i2p/util/RandomSource.java
@@ -142,7 +142,7 @@ public class RandomSource extends SecureRandom implements EntropyHarvester {
     private static final String SEEDFILE = "prngseed.rnd";
     
     public static final void writeSeed(byte buf[]) {
-        File f = new File(SEEDFILE);
+        File f = new File(I2PAppContext.getGlobalContext().getConfigDir(), SEEDFILE);
         FileOutputStream fos = null;
         try {
             fos = new FileOutputStream(f);
@@ -164,7 +164,7 @@ public class RandomSource extends SecureRandom implements EntropyHarvester {
     }
     
     private static final boolean seedFromFile(String filename, byte buf[]) {
-        File f = new File(filename);
+        File f = new File(I2PAppContext.getGlobalContext().getConfigDir(), filename);
         if (f.exists()) {
             FileInputStream fis = null;
             try {
diff --git a/core/java/src/net/i2p/util/WorkingDir.java b/core/java/src/net/i2p/util/WorkingDir.java
new file mode 100644
index 0000000000..4e47f6458c
--- /dev/null
+++ b/core/java/src/net/i2p/util/WorkingDir.java
@@ -0,0 +1,481 @@
+package net.i2p.util;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Properties;
+
+import net.i2p.data.DataHelper;
+
+/**
+ * Get a working directory for i2p.
+ *
+ * For the location, first try the system property i2p.dir.config
+ * Next try $HOME/.i2p on linux or %APPDATA%\I2P on Windows.
+ *
+ * If the dir exists, return it.
+ * Otherwise, attempt to create it, and copy files from the base directory.
+ * To successfully copy, the base install dir must be the system property i2p.dir.base
+ * or else must be in $CWD.
+ *
+ * If I2P was run from the install directory in the past,
+ * and migrateOldData = true, copy the
+ * necessary data files (except i2psnark/) over to the new working directory.
+ *
+ * Otherwise, just copy over a limited number of files over.
+ *
+ * Do not ever copy or move the old i2psnark/ directory, as if the
+ * old and new locations are on different file systems, this could
+ * be quite slow.
+ *
+ * Modify some files while copying, see methods below.
+ *
+ * After migration, the router will run using the new directory.
+ * The wrapper, however, must be stopped and restarted from the new script - until then,
+ * it will continue to write to wrapper.log* in the old directory.
+ *
+ * @param whether to copy all data over from an existing install
+ */
+public class WorkingDir {
+
+    private final static String PROP_BASE_DIR = "i2p.dir.base";
+    private final static String PROP_WORKING_DIR = "i2p.dir.config";
+    private final static String WORKING_DIR_DEFAULT_WINDOWS = "I2P";
+    private final static String WORKING_DIR_DEFAULT = ".i2p";
+
+    /**
+     * Only call this once on router invocation.
+     * Caller should store the return value for future reference.
+     */
+    public static String getWorkingDir(boolean migrateOldData) {
+        String dir = System.getProperty(PROP_WORKING_DIR);
+        boolean isWindows = System.getProperty("os.name").startsWith("Win");
+        File dirf = null;
+        if (dir != null) {
+            dirf = new File(dir);
+        } else {
+            String home = System.getProperty("user.home");
+            if (isWindows) {
+                String appdata = System.getenv("APPDATA");
+                if (appdata != null)
+                    home = appdata;
+                dirf = new File(home, WORKING_DIR_DEFAULT_WINDOWS);
+            } else {
+                dirf = new File(home, WORKING_DIR_DEFAULT);
+            }
+        }
+        // where we are now
+        String cwd = System.getProperty(PROP_BASE_DIR);
+        if (cwd == null)
+            cwd = System.getProperty("user.dir");
+        // where we want to go
+        String rv = dirf.getAbsolutePath();
+        if (dirf.exists()) {
+            if (dirf.isDirectory())
+                return rv; // all is good, we found the user directory
+            System.err.println("Wanted to use " + rv + " for a working directory but it is not a directory");
+            return cwd;
+        }
+        if (!dirf.mkdir()) {
+            System.err.println("Wanted to use " + rv + " for a working directory but could not create it");
+            return cwd;
+        }
+
+        // Check for a hosts.txt file, if it exists then I2P is there
+        File oldDirf = new File(cwd);
+        File test = new File(oldDirf, "hosts.txt");
+        if (!test.exists()) {
+            System.err.println("ERROR - Cannot find I2P installation in " + cwd);
+            return cwd;
+        }
+
+        // Check for a router.keys file, if it exists it's an old install,
+        // and only migrate the data files if told to do so
+        test = new File(oldDirf, "router.keys");
+        boolean oldInstall = test.exists();
+        migrateOldData &= oldInstall;
+
+        // Do the copying
+        if (migrateOldData)
+            System.err.println("Migrating data files to new user directory " + rv);
+        else
+            System.err.println("Setting up new user directory " + rv);
+        boolean success = migrate(MIGRATE_BASE, oldDirf, dirf);
+        // this one must be after MIGRATE_BASE
+        success &= migrateJettyXml(oldDirf, dirf);
+        success &= migrateWrapperConfig(oldDirf, dirf);
+        if (migrateOldData) {
+            success &= migrate(MIGRATE_DATA, oldDirf, dirf);
+            success &= migrateI2PTunnelKeys(oldDirf, dirf);
+            success &= migrateSnark(oldDirf, dirf);
+            // new installs will have updated scripts left in the install dir
+            // don't bother migrating runplain.sh or i2prouter.bat
+            if (!isWindows)
+                success &= migrateI2prouter(oldDirf, dirf);
+        } else if (!oldInstall) {
+            // copy the default i2psnark.config over
+            success &= migrate("i2psnark.config", oldDirf, dirf);
+        }
+
+        // Report success or failure
+        if (success) {
+            System.err.println("Successfully copied data files to new user directory " + rv);
+            if (migrateOldData) {
+                System.err.println("Libraries and other files remain in the old directory " + cwd + ", do not remove them.");
+                System.err.println("You should manually move any non-standard files, such as additional eepsite directories and key files");
+                System.err.println("After verifying that all is working, you may delete the following data files and directories in " +
+                           cwd + ": " + MIGRATE_DATA.replace(',', ' ') + " i2psnark.config tmp work");
+                if (System.getProperty("wrapper.version") != null)
+                    System.err.println("Note that until you shutdown your router completely and restart, the wrapper will continue" +
+                               " to log to the old wrapper logs in " + cwd);
+                if (!isWindows)
+                    System.err.println("From now on, you should now use the i2prouter" +
+                               " script in the " + rv + " directory to start i2p." +
+                               " You may copy or move this script elsewhere, you need not run it from that directory.");
+            }
+            return rv;
+        } else {
+            System.err.println("FAILED copy of some or all data files to new directory " + rv);
+            System.err.println("Check logs for details");
+            System.err.println("Continung to use data files in old directory " + cwd);
+            return cwd;
+        }
+    }
+
+    /**
+     * files and directories from the base install to copy over
+     * None of these should be included in i2pupdate.zip
+     *
+     * The user should not delete these in the old location, leave them as templates for new users
+     */
+    private static final String MIGRATE_BASE =
+        // base install - dirs
+        // We don't currently have a default addressbook/ in the base distribution,
+        // but distros might put one in
+        "addressbook,eepsite," +
+        // base install - files
+        // We don't currently have a default router.config or logger.config in the base distribution,
+        // but distros might put one in
+        "blocklist.txt,clients.config,hosts.txt,i2ptunnel.config,jetty-i2psnark.xml," +
+        "logger.config,router.config,systray.config";
+
+    /**
+     * files and directories from an old single-directory installation to copy over  - NOT including snark
+     * None of these should be included in i2pupdate.zip
+     *
+     * The user can be advised to delete these from the old location
+     */
+    private static final String MIGRATE_DATA =
+        // post install - dirs
+        // not required to copy - tmp/, work/
+        // addressbook included in MIGRATE_BASE above
+        "keyBackup,logs,netDb,peerProfiles," +
+        // post install - files
+        // not required to copy - prngseed.rnd
+        // logger.config and router.config included in MIGRATE_BASE above
+        "bob.config,privatehosts.txt,router.info,router.keys," +
+        "sam.keys,susimail.config,userhosts.txt,webapps.config," +
+        "wrapper.log,wrapper.log.1,wrapper.log.2";
+
+    private static boolean migrate(String list, File olddir, File todir) {
+        boolean rv = true;
+        String files[] = list.split(",");
+        for (int i = 0; i < files.length; i++) {
+            File from = new File(olddir, files[i]);
+            if (!copy(from, todir)) {
+                System.err.println("Error copying " + from.getAbsolutePath());
+                rv = false;
+            }
+        }
+        return rv;
+    }
+
+    /**
+     *  Copy over the i2psnark.config file with modifications
+     */
+    private static boolean migrateSnark(File olddir, File todir) {
+        boolean rv = true;
+        File oldSnark = new File(olddir, "i2psnark");
+        File newSnark = new File(todir, "i2psnark");
+        File oldSnarkConfig = new File(olddir, "i2psnark.config");
+        File newSnarkConfig = new File(todir, "i2psnark.config");
+        boolean hasData = oldSnark.exists();
+        if (hasData) {
+            File children[] = oldSnark.listFiles();
+            hasData = children != null && children.length > 0;
+        }
+        if (oldSnarkConfig.exists()) {
+            if (hasData) {
+                // edit the snark config file to point to the old location, we aren't moving the data
+                try {
+                    Properties props = new Properties();
+                    DataHelper.loadProps(props, oldSnarkConfig);
+                    String dir = props.getProperty("i2psnark.dir");
+                    if (dir == null)
+                        dir = "i2psnark";
+                    // change relative to absolute path
+                    File f = new File(dir);
+                    props.setProperty("i2psnark.dir", f.getAbsolutePath());
+                    DataHelper.storeProps(props, newSnarkConfig);
+                    System.err.println("Copied i2psnark.config with modifications");
+                } catch (IOException ioe) {
+                    System.err.println("FAILED copy i2psnark.config");
+                    rv = false;
+                }
+            } else {
+                // copy the i2psnark config file over
+                copy(newSnarkConfig, todir);
+                System.err.println("Copied i2psnark.config");
+            }
+        } else {
+            if (hasData) {
+                // data but no previous config file (unlikely) - make new config file
+                try {
+                    Properties props = new Properties();
+                    File f = new File("i2psnark");
+                    props.setProperty("i2psnark.dir", f.getAbsolutePath());
+                    DataHelper.storeProps(props, newSnarkConfig);
+                } catch (IOException ioe) {
+                    // ignore
+                }
+            } // else no config and no data
+        }
+        if (hasData) {
+          /*************
+            // crude attempt to detect same filesystem
+            if ((oldSnark.getAbsolutePath().startsWith("/home/") && newSnark.getAbsolutePath().startsWith("/home/")) ||
+                (System.getProperty("os.name").toLowerCase.indexOf("windows") >= 0 &&
+                 oldSnark.getAbsolutePath().substring(0,1).equals(newSnark.getAbsolutePath().substring(0,1) &&
+                 oldSnark.getAbsolutePath().substring(1,2).equals(":\\") &&
+                 newSnark.getAbsolutePath().substring(1,2).equals(":\\"))) {
+                 // OK, apparently in same file system
+                 // move everything
+            }
+          **************/
+            System.err.println("NOT moving the i2psnark data directory " + oldSnark.getAbsolutePath() +
+                   " to the new directory " + newSnark.getAbsolutePath() +
+                   ". You may move the directory contents manually WHILE I2P IS NOT RUNNING," +
+                   " and edit the file " + newSnarkConfig.getAbsolutePath() +
+                   " to configure i2psnark to use a different location by editing the i2psnark.dir configuration to be" +
+                   " i2psnark.dir=" + oldSnark.getAbsolutePath() +
+                   " and restart, or you may leave the i2psnark directory in its old location.");
+        }
+        return true;
+    }
+
+    /**
+     *  Copy over the i2prouter file with modifications
+     *  The resulting script can be run from any location.
+     */
+    private static boolean migrateI2prouter(File olddir, File todir) {
+        File oldFile = new File(olddir, "i2prouter");
+        File newFile = new File(todir, "i2prouter");
+        FileInputStream in = null;
+        PrintWriter out = null;
+        try {
+            in = new FileInputStream(oldFile);
+            out = new PrintWriter(new BufferedWriter(new FileWriter(newFile)));
+            boolean firstTime = true;
+            String s = null;
+            while ((s = DataHelper.readLine(in)) != null) {
+                if (s.equals("WRAPPER_CMD=\"./i2psvc\"")) {
+                    // i2psvc in the old location
+                    File f = new File("i2psvc");
+                    s = "WRAPPER_CMD=\"" + f.getAbsolutePath() + "\"";
+                } else if(s.equals("WRAPPER_CONF=\"wrapper.config\"")) {
+                    // wrapper.config the new location
+                    File f = new File(todir, "wrapper.config");
+                    s = "WRAPPER_CONF=\"" + f.getAbsolutePath() + "\"";
+                } else if(s.equals("PIDDIR=\".\"")) {
+                    // i2p.pid in the new location
+                    s = "PIDDIR=\"" + todir.getAbsolutePath() + "\"";
+                }
+                out.println(s);
+                if (firstTime) {
+                    // first line was #!/bin/sh, so had to wait until now
+                    out.println("# Modified by I2P User dir migration script");
+                    firstTime = false;
+                }            
+            }            
+            System.err.println("Copied i2prouter with modifications");
+            return true;
+        } catch (IOException ioe) {
+            if (in != null) {
+                System.err.println("FAILED copy i2prouter");
+                return false;
+            }
+            return true;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) out.close();
+        }
+    }
+
+    /**
+     *  Copy over the wrapper.config file with modifications
+     */
+    private static boolean migrateWrapperConfig(File olddir, File todir) {
+        File oldFile = new File(olddir, "wrapper.config");
+        File newFile = new File(todir, "wrapper.config");
+        FileInputStream in = null;
+        PrintWriter out = null;
+        try {
+            in = new FileInputStream(oldFile);
+            out = new PrintWriter(new BufferedWriter(new FileWriter(newFile)));
+            out.println("# Modified by I2P User dir migration script");
+            String s = null;
+            // Don't use replaceFirst because backslashes in the replacement string leads to havoc
+            // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4689750
+            // "Note that backslashes and dollar signs in the replacement string may cause the results
+            // to be different than if it were being treated as a literal replacement string.
+            // Dollar signs may be treated as references to captured subsequences as described above,
+            // and backslashes are used to escape literal characters in the replacement string."
+            while ((s = DataHelper.readLine(in)) != null) {
+                if (s.startsWith("wrapper.java.classpath.")) {
+                    // libraries in the old location
+                    s = s.replace("=lib/", '=' + olddir.getAbsolutePath() + File.separatorChar + "lib" + File.separatorChar);
+                } else if (s.startsWith("wrapper.java.library.path.")) {
+                    // libraries in the old location
+                    if (s.contains("=."))
+                        s = s.replace("=.", '=' + olddir.getAbsolutePath());
+                    else if (s.contains("=lib"))
+                        s = s.replace("=lib", '=' + olddir.getAbsolutePath() + File.separatorChar + "lib");
+                } else if (s.startsWith("wrapper.logfile=wrapper.log")) {
+                    // wrapper logs in the new location
+                    s = s.replace("=", '=' + todir.getAbsolutePath() + File.separatorChar);
+                } else if (s.startsWith("wrapper.pidfile=i2p.pid")) {
+                    // i2p.pid in the new location
+                    s = s.replace("=", '=' + todir.getAbsolutePath() + File.separatorChar);
+                }
+                out.println(s);
+            }
+            System.err.println("Copied wrapper.config with modifications");
+            return true;
+        } catch (IOException ioe) {
+            if (in != null) {
+                System.err.println("FAILED copy wrapper.config");
+                return false;
+            }
+            return false;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) out.close();
+        }
+    }
+
+    /**
+     *  Copy over the jetty.xml file with modifications
+     *  It was already copied over once in migrate(), throw that out and
+     *  do it again with modifications.
+     */
+    private static boolean migrateJettyXml(File olddir, File todir) {
+        File eepsite1 = new File(olddir, "eepsite");
+        File oldFile = new File(eepsite1, "jetty.xml");
+        File eepsite2 = new File(todir, "eepsite");
+        File newFile = new File(eepsite2, "jetty.xml");
+        FileInputStream in = null;
+        PrintWriter out = null;
+        try {
+            in = new FileInputStream(oldFile);
+            out = new PrintWriter(new BufferedWriter(new FileWriter(newFile)));
+            String s = null;
+            while ((s = DataHelper.readLine(in)) != null) {
+                if (s.indexOf("./eepsite/") >= 0) {
+                    s = s.replace("./eepsite/", todir.getAbsolutePath() + File.separatorChar + "eepsite" + File.separatorChar);
+                }
+                out.println(s);
+            }
+            out.println("<!-- Modified by I2P User dir migration script -->");
+            System.err.println("Copied jetty.xml with modifications");
+            return true;
+        } catch (IOException ioe) {
+            if (in != null) {
+                System.err.println("FAILED copy jetty.xml");
+                return false;
+            }
+            return false;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) out.close();
+        }
+    }
+
+    /**
+     * Relatively recent default i2ptunnel key file name
+     */
+    private static boolean migrateI2PTunnelKeys(File olddir, File todir) {
+        for (int i = 0; i < 100; i++) {
+            copy(new File(olddir, "i2ptunnel" + i + "-privKeys.dat"), todir);
+        }
+        return true;
+    }
+
+    /**
+     * Recursive copy a file or dir to a dir
+     * 
+     * @param src file or directory, need not exist
+     * @param target the directory to copy to, will be created if it doesn't exist
+     * @return true for success OR if src does not exist
+     */
+    public static final boolean copy(File src, File targetDir) {
+        if (!src.exists())
+            return true;
+        if (!targetDir.exists()) {
+            if (!targetDir.mkdir()) {
+                System.err.println("FAILED copy " + src.getPath());
+                return false;
+            }
+        }
+        File targetFile = new File(targetDir, src.getName());
+        if (!src.isDirectory())
+            return copyFile(src, targetFile);
+        File children[] = src.listFiles();
+        if (children == null) {
+            System.err.println("FAILED copy " + src.getPath());
+            return false;
+        }
+        boolean rv = true;
+        for (int i = 0; i < children.length; i++) {
+            rv &= copy(children[i], targetFile);
+        }
+        return rv;
+    }
+    
+    /**
+     * @param src not a directory, must exist
+     * @param dest not a directory, will be overwritten if existing
+     * @@reurn true if it was copied successfully
+     */
+    public static boolean copyFile(File src, File dst) {
+        if (!src.exists()) return false;
+        boolean rv = true;
+
+        byte buf[] = new byte[4096];
+        FileInputStream in = null;
+        FileOutputStream out = null;
+        try {
+            in = new FileInputStream(src);
+            out = new FileOutputStream(dst);
+            
+            int read = 0;
+            while ( (read = in.read(buf)) != -1)
+                out.write(buf, 0, read);
+            
+            System.err.println("Copied " + src.getPath());
+        } catch (IOException ioe) {
+            System.err.println("FAILED copy " + src.getPath());
+            rv = false;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) try { out.close(); } catch (IOException ioe) {}
+        }
+        if (rv)
+            dst.setLastModified(src.lastModified());
+        return rv;
+    }
+}
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
index 7546ac4a71..6195d6a769 100644
--- a/router/java/src/net/i2p/router/Blocklist.java
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -166,6 +166,8 @@ public class Blocklist {
     */
     private void readBlocklistFile(String file) {
         File BLFile = new File(file);
+        if (!BLFile.isAbsolute())
+            BLFile = new File(_context.getConfigDir(), file);
         if (BLFile == null || (!BLFile.exists()) || BLFile.length() <= 0) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Blocklist file not found: " + file);
@@ -701,6 +703,8 @@ public class Blocklist {
     private synchronized void shitlistForever(Hash peer) {
         String file = _context.getProperty(PROP_BLOCKLIST_FILE, BLOCKLIST_FILE_DEFAULT);
         File BLFile = new File(file);
+        if (!BLFile.isAbsolute())
+            BLFile = new File(_context.getConfigDir(), file);
         if (BLFile == null || (!BLFile.exists()) || BLFile.length() <= 0) {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("Blocklist file not found: " + file);
diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java
index 9fc62a70f2..711759a408 100644
--- a/router/java/src/net/i2p/router/KeyManager.java
+++ b/router/java/src/net/i2p/router/KeyManager.java
@@ -147,10 +147,8 @@ public class KeyManager {
             super(KeyManager.this._context);
         }
         public void runJob() {
-            String keyDir = getContext().getProperty(PROP_KEYDIR);
-            if (keyDir == null) 
-                keyDir = DEFAULT_KEYDIR;
-            File dir = new File(keyDir);
+            String keyDir = getContext().getProperty(PROP_KEYDIR, DEFAULT_KEYDIR);
+            File dir = new File(getContext().getRouterDir(), keyDir);
             if (!dir.exists())
                 dir.mkdirs();
             if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) {
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 1e8e905528..35518feda3 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -44,6 +44,7 @@ import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
+import net.i2p.util.WorkingDir;
 
 /**
  * Main driver for the router.
@@ -53,6 +54,7 @@ public class Router {
     private Log _log;
     private RouterContext _context;
     private final Properties _config;
+    /** full path */
     private String _configFilename;
     private RouterInfo _routerInfo;
     private long _started;
@@ -104,14 +106,6 @@ public class Router {
     public Router(Properties envProps) { this(null, envProps); }
     public Router(String configFilename) { this(configFilename, null); }
     public Router(String configFilename, Properties envProps) {
-        if (!beginMarkingLiveliness(envProps)) {
-            System.err.println("ERROR: There appears to be another router already running!");
-            System.err.println("       Please make sure to shut down old instances before starting up");
-            System.err.println("       a new one.  If you are positive that no other instance is running,");
-            System.err.println("       please delete the file " + getPingFile(envProps));
-            System.exit(-1);
-        }
-
         _gracefulExitCode = -1;
         _config = new Properties();
 
@@ -125,7 +119,35 @@ public class Router {
             _configFilename = configFilename;
         }
                     
+        // we need the user directory figured out by now, so figure it out here rather than
+        // in the RouterContext() constructor.
+        //
+        // Fixme read config file before migration? or before? or both?
+        //
+        // Then add it to envProps (but not _config, we don't want it in the router.config file)
+        // where it will then be available to all via _context.dir()
+        //
+        // This call also migrates all files to the new working directory,
+        // including router.config
+        //
+
+        // Do we copy all the data files to the new directory? default false
+        String migrate = System.getProperty("i2p.dir.migrate");
+        boolean migrateFiles = Boolean.valueOf(migrate).booleanValue();
+        String userDir = WorkingDir.getWorkingDir(migrateFiles);
+
+        // Use the router.config file specified in the router.configLocation property
+        // (default "router.config"),
+        // if it is an abolute path, otherwise look in the userDir returned by getWorkingDir
+        // replace relative path with absolute
+        File cf = new File(_configFilename);
+        if (!cf.isAbsolute()) {
+            cf = new File(userDir, _configFilename);
+            _configFilename = cf.getAbsolutePath();
+        }
+
         readConfig();
+
         if (envProps == null) {
             envProps = _config;
         } else {
@@ -135,11 +157,42 @@ public class Router {
                 envProps.setProperty(k, v);
             }
         }
+
         // This doesn't work, guess it has to be in the static block above?
         // if (Boolean.valueOf(envProps.getProperty("router.disableIPv6")).booleanValue())
         //    System.setProperty("java.net.preferIPv4Stack", "true");
 
+        if (envProps.getProperty("i2p.dir.config") == null)
+            envProps.setProperty("i2p.dir.config", userDir);
+
+        // The important thing that happens here is the directory paths are set and created
+        // i2p.dir.router defaults to i2p.dir.config
+        // i2p.dir.app defaults to i2p.dir.router
+        // i2p.dir.log defaults to i2p.dir.router
+        // i2p.dir.pid defaults to i2p.dir.router
+        // i2p.dir.base defaults to user.dir == $CWD
         _context = new RouterContext(this, envProps);
+
+        // This is here so that we can get the directory location from the context
+        // for the ping file
+        if (!beginMarkingLiveliness()) {
+            System.err.println("ERROR: There appears to be another router already running!");
+            System.err.println("       Please make sure to shut down old instances before starting up");
+            System.err.println("       a new one.  If you are positive that no other instance is running,");
+            System.err.println("       please delete the file " + getPingFile().getAbsolutePath());
+            System.exit(-1);
+        }
+
+        // This is here so that we can get the directory location from the context
+        // for the zip file and the base location to unzip to.
+        // If it does an update, it never returns.
+        // I guess it's better to have the other-router check above this, we don't want to
+        // overwrite an existing running router's jar files. Other than ours.
+        installUpdates();
+
+        // NOW we start all the activity
+        _context.initAll();
+
         _routerInfo = null;
         _higherVersionSeen = false;
         _log = _context.logManager().getLog(Router.class);
@@ -245,6 +298,7 @@ public class Router {
         
         _context.keyManager().startup();
         
+        // why are we reading this again, it's read in the constructor
         readConfig();
         
         setupHandlers();
@@ -285,6 +339,7 @@ public class Router {
         }
     }
     
+    /** this does not use ctx.getConfigDir(), must provide a full path in filename */
     private static Properties getConfig(RouterContext ctx, String filename) {
         Log log = null;
         if (ctx != null) {
@@ -456,11 +511,11 @@ public class Router {
                                                                };
 
     static final String IDENTLOG = "identlog.txt";
-    public static void killKeys() {
+    public void killKeys() {
         new Exception("Clearing identity files").printStackTrace();
         int remCount = 0;
         for (int i = 0; i < _rebuildFiles.length; i++) {
-            File f = new File(_rebuildFiles[i]);
+            File f = new File(_context.getRouterDir(),_rebuildFiles[i]);
             if (f.exists()) {
                 boolean removed = f.delete();
                 if (removed) {
@@ -474,7 +529,7 @@ public class Router {
         if (remCount > 0) {
             FileOutputStream log = null;
             try {
-                log = new FileOutputStream(IDENTLOG, true);
+                log = new FileOutputStream(new File(_context.getRouterDir(), IDENTLOG), true);
                 log.write((new Date() + ": Old router identity keys cleared\n").getBytes());
             } catch (IOException ioe) {
                 // ignore
@@ -814,8 +869,9 @@ public class Router {
         try { _context.messageValidator().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the message validator", t); }
         try { _context.inNetMessagePool().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the inbound net pool", t); }
         //try { _sessionKeyPersistenceHelper.shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the session key manager", t); }
+        _context.deleteTempDir();
         RouterContext.listContexts().remove(_context);
-        dumpStats();
+        //dumpStats();
         finalShutdown(exitCode);
     }
 
@@ -833,7 +889,7 @@ public class Router {
                 killKeys();
         }
 
-        File f = new File(getPingFile());
+        File f = getPingFile();
         f.delete();
         if (_killVMOnEnd) {
             try { Thread.sleep(1000); } catch (InterruptedException ie) {}
@@ -1003,8 +1059,9 @@ public class Router {
     
     public static void main(String args[]) {
         System.out.println("Starting I2P " + RouterVersion.FULL_VERSION);
-        installUpdates();
-        verifyWrapperConfig();
+        // installUpdates() moved to constructor so we can get file locations from the context
+        // installUpdates();
+        //verifyWrapperConfig();
         Router r = new Router();
         if ( (args != null) && (args.length == 1) && ("rebuild".equals(args[0])) ) {
             r.rebuildNewIdentity();
@@ -1015,11 +1072,30 @@ public class Router {
     
     public static final String UPDATE_FILE = "i2pupdate.zip";
     
-    private static void installUpdates() {
-        File updateFile = new File(UPDATE_FILE);
-        if (updateFile.exists()) {
+    /**
+     * Unzip update file found in the router dir OR base dir, to the base dir
+     *
+     * If we can't write to the base dir, complain.
+     */
+    private void installUpdates() {
+        File updateFile = new File(_context.getRouterDir(), UPDATE_FILE);
+        boolean exists = updateFile.exists();
+        if (!exists) {
+            updateFile = new File(_context.getBaseDir(), UPDATE_FILE);
+            exists = updateFile.exists();
+        }
+        if (exists) {
+            File test = new File(_context.getBaseDir(), "history.txt");
+            if ((!test.canWrite()) || (!_context.getBaseDir().canWrite())) {
+                String msg = "ERROR: No write permissions on " + _context.getBaseDir() +
+                             " to extract software update file";
+                System.out.println(msg);
+                _log.log(Log.CRIT, msg);
+                // carry on
+                return;
+            }
             System.out.println("INFO: Update file exists [" + UPDATE_FILE + "] - installing");
-            boolean ok = FileUtil.extractZip(updateFile, new File("."));
+            boolean ok = FileUtil.extractZip(updateFile, _context.getBaseDir());
             if (ok)
                 System.out.println("INFO: Update installed");
             else
@@ -1037,6 +1113,7 @@ public class Router {
         }
     }
     
+/*******
     private static void verifyWrapperConfig() {
         File cfgUpdated = new File("wrapper.config.updated");
         if (cfgUpdated.exists()) {
@@ -1046,15 +1123,22 @@ public class Router {
             System.exit(EXIT_HARD);
         }
     }
+*******/
     
+/*
     private static String getPingFile(Properties envProps) {
         if (envProps != null) 
             return envProps.getProperty("router.pingFile", "router.ping");
         else
             return "router.ping";
     }
-    private String getPingFile() {
-        return _context.getProperty("router.pingFile", "router.ping");
+*/
+    private File getPingFile() {
+        String s = _context.getProperty("router.pingFile", "router.ping");
+        File f = new File(s);
+        if (!f.isAbsolute())
+            f = new File(_context.getPIDDir(), s);
+        return f;
     }
     
     static final long LIVELINESS_DELAY = 60*1000;
@@ -1066,9 +1150,8 @@ public class Router {
      * 
      * @return true if the router is the only one running 
      */
-    private boolean beginMarkingLiveliness(Properties envProps) {
-        String filename = getPingFile(envProps);
-        File f = new File(filename);
+    private boolean beginMarkingLiveliness() {
+        File f = getPingFile();
         if (f.exists()) {
             long lastWritten = f.lastModified();
             if (System.currentTimeMillis()-lastWritten > LIVELINESS_DELAY) {
@@ -1376,15 +1459,14 @@ private static class PersistRouterInfoJob extends JobImpl {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Persisting updated router info");
 
-        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME);
-        if (infoFilename == null)
-            infoFilename = Router.PROP_INFO_FILENAME_DEFAULT;
+        String infoFilename = getContext().getProperty(PROP_INFO_FILENAME, PROP_INFO_FILENAME_DEFAULT);
+        File infoFile = new File(getContext().getRouterDir(), infoFilename);
 
         RouterInfo info = getContext().router().getRouterInfo();
 
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(infoFilename);
+            fos = new FileOutputStream(infoFile);
             info.writeBytes(fos);
         } catch (DataFormatException dfe) {
             _log.error("Error rebuilding the router information", dfe);
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 719224058b..656b1299bd 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -70,7 +70,11 @@ public class RouterContext extends I2PAppContext {
     public RouterContext(Router router, Properties envProps) { 
         super(filterProps(envProps));
         _router = router;
-        initAll();
+        // Disabled here so that the router can get a context and get the
+        // directory locations from it, to do an update, without having
+        // to init everything. Caller MUST call initAll() afterwards.
+        // Sorry, this breaks some main() unit tests out there.
+        //initAll();
         _contexts.add(this);
     }
     /**
@@ -86,7 +90,7 @@ public class RouterContext extends I2PAppContext {
             envProps.setProperty("time.disabled", "false");
         return envProps;
     }
-    private void initAll() {
+    public void initAll() {
         //_adminManager = new AdminManager(this);
         if ("false".equals(getProperty("i2p.dummyClientFacade", "false")))
             _clientManagerFacade = new ClientManagerFacadeImpl(this);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
index 282b67e0cd..92124d6901 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
@@ -410,7 +410,7 @@ class PersistentDataStore extends TransientDataStore {
     
     
     private File getDbDir() throws IOException {
-        File f = new File(_dbDir);
+        File f = new File(_context.getRouterDir(), _dbDir);
         if (!f.exists()) {
             boolean created = f.mkdirs();
             if (!created)
diff --git a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
index 4f5d236116..e1f265d8f1 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
@@ -297,18 +297,8 @@ class ProfilePersistenceHelper {
     
     private File getProfileDir() {
         if (_profileDir == null) {
-            String dir = null;
-            if (_context.router() == null) {
-                dir = _context.getProperty(PROP_PEER_PROFILE_DIR, DEFAULT_PEER_PROFILE_DIR);
-            } else {
-                dir = _context.router().getConfigSetting(PROP_PEER_PROFILE_DIR);
-                if (dir == null) {
-                    _log.info("No peer profile dir specified [" + PROP_PEER_PROFILE_DIR 
-                              + "], using [" + DEFAULT_PEER_PROFILE_DIR + "]");
-                    dir = DEFAULT_PEER_PROFILE_DIR;
-                }
-            }
-            _profileDir = new File(dir);
+            String dir = _context.getProperty(PROP_PEER_PROFILE_DIR, DEFAULT_PEER_PROFILE_DIR);
+            _profileDir = new File(_context.getRouterDir(), dir);
         }
         return _profileDir;
     }
diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
index 316c0b5006..f5d48cb1f2 100644
--- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java
+++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
@@ -43,6 +43,8 @@ public class ClientAppConfig {
         Properties rv = new Properties();
         String clientConfigFile = ctx.getProperty(PROP_CLIENT_CONFIG_FILENAME, DEFAULT_CLIENT_CONFIG_FILENAME);
         File cfgFile = new File(clientConfigFile);
+        if (!cfgFile.isAbsolute())
+            cfgFile = new File(ctx.getConfigDir(), clientConfigFile);
         
         // fall back to use router.config's clientApp.* lines
         if (!cfgFile.exists()) 
@@ -91,9 +93,12 @@ public class ClientAppConfig {
 
     public static void writeClientAppConfig(RouterContext ctx, List apps) {
         String clientConfigFile = ctx.getProperty(PROP_CLIENT_CONFIG_FILENAME, DEFAULT_CLIENT_CONFIG_FILENAME);
+        File cfgFile = new File(clientConfigFile);
+        if (!cfgFile.isAbsolute())
+            cfgFile = new File(ctx.getConfigDir(), clientConfigFile);
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(clientConfigFile);
+            fos = new FileOutputStream(cfgFile);
             StringBuffer buf = new StringBuffer(2048);
             for(int i = 0; i < apps.size(); i++) {
                 ClientAppConfig app = (ClientAppConfig) apps.get(i);
diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
index 92b176c30f..979cefa78f 100644
--- a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
@@ -8,6 +8,7 @@ package net.i2p.router.startup;
  *
  */
 
+import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.HashSet;
@@ -76,16 +77,14 @@ public class CreateRouterInfoJob extends JobImpl {
             
             info.sign(signingPrivKey);
             
-            String infoFilename = getContext().router().getConfigSetting(Router.PROP_INFO_FILENAME);
-            if (infoFilename == null)
-                infoFilename = Router.PROP_INFO_FILENAME_DEFAULT;
-            fos1 = new FileOutputStream(infoFilename);
+            String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
+            File ifile = new File(getContext().getRouterDir(), infoFilename);
+            fos1 = new FileOutputStream(ifile);
             info.writeBytes(fos1);
             
-            String keyFilename = getContext().router().getConfigSetting(Router.PROP_KEYS_FILENAME);
-            if (keyFilename == null)
-                keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT;
-            fos2 = new FileOutputStream(keyFilename);
+            String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
+            File kfile = new File(getContext().getRouterDir(), keyFilename);
+            fos2 = new FileOutputStream(kfile);
             privkey.writeBytes(fos2);
             signingPrivKey.writeBytes(fos2);
             pubkey.writeBytes(fos2);
@@ -96,7 +95,7 @@ public class CreateRouterInfoJob extends JobImpl {
             getContext().keyManager().setPrivateKey(privkey);
             getContext().keyManager().setPublicKey(pubkey);
             
-            _log.info("Router info created and stored at " + infoFilename + " with private keys stored at " + keyFilename + " [" + info + "]");
+            _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
         } catch (DataFormatException dfe) {
             _log.error("Error building the new router information", dfe);
         } catch (IOException ioe) {
diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
index 0374922afd..f3428c4474 100644
--- a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
@@ -51,21 +51,17 @@ public class LoadRouterInfoJob extends JobImpl {
     }
     
     private void loadRouterInfo() {
-        String routerInfoFile = getContext().router().getConfigSetting(Router.PROP_INFO_FILENAME);
-        if (routerInfoFile == null)
-            routerInfoFile = Router.PROP_INFO_FILENAME_DEFAULT;
+        String routerInfoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
         RouterInfo info = null;
         boolean failedRead = false;
         
         
-        String keyFilename = getContext().router().getConfigSetting(Router.PROP_KEYS_FILENAME);
-        if (keyFilename == null)
-            keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT;
+        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
         
-        File rif = new File(routerInfoFile);
+        File rif = new File(getContext().getRouterDir(), routerInfoFile);
         if (rif.exists())
             _infoExists = true;
-        File rkf = new File(keyFilename);
+        File rkf = new File(getContext().getRouterDir(), keyFilename);
         if (rkf.exists())
             _keysExist = true;
         
@@ -98,14 +94,14 @@ public class LoadRouterInfoJob extends JobImpl {
             
             _us = info;
         } catch (IOException ioe) {
-            _log.error("Error reading the router info from " + routerInfoFile + " and the keys from " + keyFilename, ioe);
+            _log.error("Error reading the router info from " + rif.getAbsolutePath() + " and the keys from " + rkf.getAbsolutePath(), ioe);
             _us = null;
             rif.delete();
             rkf.delete();
             _infoExists = false;
             _keysExist = false;
         } catch (DataFormatException dfe) {
-            _log.error("Corrupt router info or keys at " + routerInfoFile + " / " + keyFilename, dfe);
+            _log.error("Corrupt router info or keys at " + rif.getAbsolutePath() + " / " + rkf.getAbsolutePath(), dfe);
             _us = null;
             rif.delete();
             rkf.delete();
diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
index 967bc7a797..fda82de8e5 100644
--- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
@@ -57,18 +57,11 @@ public class RebuildRouterInfoJob extends JobImpl {
     
     public void runJob() {
         _log.debug("Testing to rebuild router info");
-        String infoFile = getContext().router().getConfigSetting(Router.PROP_INFO_FILENAME);
-        if (infoFile == null) {
-            _log.debug("Info filename not configured, defaulting to " + Router.PROP_INFO_FILENAME_DEFAULT);
-            infoFile = Router.PROP_INFO_FILENAME_DEFAULT;
-        }
-        
-        String keyFilename = getContext().router().getConfigSetting(Router.PROP_KEYS_FILENAME);
-        if (keyFilename == null)
-            keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT;
-        File keyFile = new File(keyFilename);
+        String infoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
+        File info = new File(getContext().getRouterDir(), infoFile);
+        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
+        File keyFile = new File(getContext().getRouterDir(), keyFilename);
         
-        File info = new File(infoFile);
         if (!info.exists() || !keyFile.exists()) {
             _log.info("Router info file [" + info.getAbsolutePath() + "] or private key file [" + keyFile.getAbsolutePath() + "] deleted, rebuilding");
             rebuildRouterInfo();
@@ -86,14 +79,10 @@ public class RebuildRouterInfoJob extends JobImpl {
         _log.debug("Rebuilding the new router info");
         boolean fullRebuild = false;
         RouterInfo info = null;
-        String infoFilename = getContext().router().getConfigSetting(Router.PROP_INFO_FILENAME);
-        if (infoFilename == null)
-            infoFilename = Router.PROP_INFO_FILENAME_DEFAULT;
-        
-        String keyFilename = getContext().router().getConfigSetting(Router.PROP_KEYS_FILENAME);
-        if (keyFilename == null)
-            keyFilename = Router.PROP_KEYS_FILENAME_DEFAULT;
-        File keyFile = new File(keyFilename);
+        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
+        File infoFile = new File(getContext().getRouterDir(), infoFilename);
+        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
+        File keyFile = new File(getContext().getRouterDir(), keyFilename);
         
         if (keyFile.exists()) {
             // ok, no need to rebuild a brand new identity, just update what we can
@@ -146,7 +135,7 @@ public class RebuildRouterInfoJob extends JobImpl {
             
             FileOutputStream fos = null;
             try {
-                fos = new FileOutputStream(infoFilename);
+                fos = new FileOutputStream(infoFile);
                 info.writeBytes(fos);
             } catch (DataFormatException dfe) {
                 _log.error("Error rebuilding the router information", dfe);
diff --git a/router/java/src/net/i2p/router/transport/GeoIP.java b/router/java/src/net/i2p/router/transport/GeoIP.java
index a7da9fad86..fb4984978d 100644
--- a/router/java/src/net/i2p/router/transport/GeoIP.java
+++ b/router/java/src/net/i2p/router/transport/GeoIP.java
@@ -130,7 +130,8 @@ public class GeoIP {
     *
     */
     private void readCountryFile() {
-        File GeoFile = new File(GEOIP_DIR_DEFAULT, COUNTRY_FILE_DEFAULT);
+        File GeoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT);
+        GeoFile = new File(GeoFile, COUNTRY_FILE_DEFAULT);
         if (GeoFile == null || (!GeoFile.exists())) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Country file not found: " + GeoFile.getAbsolutePath());
@@ -188,7 +189,8 @@ public class GeoIP {
     *
     */
     private String[] readGeoIPFile(Long[] search) {
-        File GeoFile = new File(GEOIP_DIR_DEFAULT, GEOIP_FILE_DEFAULT);
+        File GeoFile = new File(_context.getBaseDir(), GEOIP_DIR_DEFAULT);
+        GeoFile = new File(GeoFile, GEOIP_FILE_DEFAULT);
         if (GeoFile == null || (!GeoFile.exists())) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("GeoIP file not found: " + GeoFile.getAbsolutePath());
-- 
GitLab