diff --git a/components/net/sf/briar/plugins/file/FilePlugin.java b/components/net/sf/briar/plugins/file/FilePlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..69537cb53d0fd414449ca6147310031324b86c66
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/FilePlugin.java
@@ -0,0 +1,121 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+
+import org.apache.commons.io.FileSystemUtils;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.TransportId;
+import net.sf.briar.api.transport.InvalidConfigException;
+import net.sf.briar.api.transport.InvalidTransportException;
+import net.sf.briar.api.transport.TransportConstants;
+import net.sf.briar.api.transport.batch.BatchTransportCallback;
+import net.sf.briar.api.transport.batch.BatchTransportPlugin;
+import net.sf.briar.api.transport.batch.BatchTransportReader;
+import net.sf.briar.api.transport.batch.BatchTransportWriter;
+
+abstract class FilePlugin implements BatchTransportPlugin {
+
+	public static final int TRANSPORT_ID = 0;
+
+	private static final TransportId id = new TransportId(TRANSPORT_ID);
+
+	private boolean started = false;
+	protected Map<String, String> localProperties = null;
+	protected Map<ContactId, Map<String, String>> remoteProperties = null;
+	protected Map<String, String> config = null;
+	protected BatchTransportCallback callback = null;
+
+	protected abstract File chooseOutputDirectory();
+	protected abstract void writerFinished(File f);
+
+	public TransportId getId() {
+		return id;
+	}
+
+	public synchronized void start(Map<String, String> localProperties,
+			Map<ContactId, Map<String, String>> remoteProperties,
+			Map<String, String> config, BatchTransportCallback callback)
+	throws InvalidTransportException, InvalidConfigException {
+		if(started) throw new IllegalStateException();
+		started = true;
+		this.localProperties = localProperties;
+		this.remoteProperties = remoteProperties;
+		this.config = config;
+		this.callback = callback;
+	}
+
+	public synchronized void stop() {
+		if(!started) throw new IllegalStateException();
+		started = false;
+	}
+
+	public synchronized void setLocalProperties(Map<String, String> properties)
+	throws InvalidTransportException {
+		if(!started) throw new IllegalStateException();
+		localProperties = properties;
+	}
+
+	public synchronized void setRemoteProperties(ContactId c,
+			Map<String, String> properties)
+	throws InvalidTransportException {
+		if(!started) throw new IllegalStateException();
+		remoteProperties.put(c, properties);
+	}
+
+	public synchronized void setConfig(Map<String, String> config)
+	throws InvalidConfigException {
+		if(!started) throw new IllegalStateException();
+		this.config = config;
+	}
+
+	public boolean shouldPoll() {
+		return false;
+	}
+
+	public int getPollingInterval() {
+		return 0;
+	}
+
+	public void poll() {
+		throw new UnsupportedOperationException();
+	}
+
+	public BatchTransportReader createReader(ContactId c) {
+		return null;
+	}
+
+	public BatchTransportWriter createWriter(ContactId c) {
+		if(!started) throw new IllegalStateException();
+		File dir = chooseOutputDirectory();
+		if(dir == null) return null;
+		if(!dir.exists()) return null;
+		if(!dir.isDirectory()) return null;
+		File f = new File(dir, createFilename());
+		try {
+			long capacity = getCapacity(f.getAbsolutePath());
+			if(capacity < TransportConstants.MIN_CONNECTION_LENGTH) return null;
+			OutputStream out = new FileOutputStream(f);
+			return new FileTransportWriter(f, out, capacity, this);
+		} catch(IOException e) {
+			f.delete();
+			return null;
+		}
+	}
+
+	protected String createFilename() {
+		StringBuilder s = new StringBuilder(12);
+		for(int i = 0; i < 8; i++) s.append((char) ('a' + Math.random() * 26));
+		s.append(".dat");
+		System.out.println(s);
+		return s.toString();
+	}
+
+	protected long getCapacity(String path) throws IOException {
+		return FileSystemUtils.freeSpaceKb(path) * 1024L;
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/FileTransportWriter.java b/components/net/sf/briar/plugins/file/FileTransportWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5d3532555b6118fedb48ec9d5bb3063614d91d5
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/FileTransportWriter.java
@@ -0,0 +1,44 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import net.sf.briar.api.transport.batch.BatchTransportWriter;
+
+class FileTransportWriter implements BatchTransportWriter {
+
+	private final File file;
+	private final OutputStream out;
+	private final long capacity;
+	private final FilePlugin plugin;
+
+	private boolean streamInUse = false;
+
+	FileTransportWriter(File file, OutputStream out, long capacity,
+			FilePlugin plugin) {
+		this.file = file;
+		this.out = out;
+		this.capacity = capacity;
+		this.plugin = plugin;
+	}
+
+	public long getCapacity() {
+		return capacity;
+	}
+
+	public OutputStream getOutputStream() {
+		streamInUse = true;
+		return out;
+	}
+
+	public void finish() {
+		streamInUse = false;
+		plugin.writerFinished(file);
+	}
+
+	public void dispose() throws IOException {
+		if(streamInUse) out.close();
+		file.delete();
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/LinuxRemovableDriveFinder.java b/components/net/sf/briar/plugins/file/LinuxRemovableDriveFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..ccdf0bc0e2d6777573fe5acbfe0cfde42aa771bb
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/LinuxRemovableDriveFinder.java
@@ -0,0 +1,21 @@
+package net.sf.briar.plugins.file;
+
+class LinuxRemovableDriveFinder extends UnixRemovableDriveFinder {
+
+	@Override
+	protected String getMountCommand() {
+		return "/bin/mount";
+	}
+
+	@Override
+	protected String parseMountPoint(String line) {
+		// The format is "/dev/foo on /bar/baz type bam (opt1,opt2)"
+		line = line.replaceFirst("^/dev/[^ ]+ on ", "");
+		return line.replaceFirst(" type [^ ]+ \\([^)]+\\)$", "");
+	}
+
+	@Override
+	protected boolean isRemovableDriveMountPoint(String path) {
+		return path.startsWith("/mnt/") || path.startsWith("/media/");
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/MacRemovableDriveFinder.java b/components/net/sf/briar/plugins/file/MacRemovableDriveFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec1ae07086c929841664ce439c9cde3dc0fdc8dc
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/MacRemovableDriveFinder.java
@@ -0,0 +1,21 @@
+package net.sf.briar.plugins.file;
+
+class MacRemovableDriveFinder extends UnixRemovableDriveFinder {
+
+	@Override
+	protected String getMountCommand() {
+		return "/sbin/mount";
+	}
+
+	@Override
+	protected String parseMountPoint(String line) {
+		// The format is "/dev/foo on /bar/baz (opt1, opt2)"
+		line = line.replaceFirst("^/dev/[^ ]+ on ", "");
+		return line.replaceFirst(" \\([^)]+\\)$", "");
+	}
+
+	@Override
+	protected boolean isRemovableDriveMountPoint(String path) {
+		return path.startsWith("/Volumes/");
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/RemovableDriveFinder.java b/components/net/sf/briar/plugins/file/RemovableDriveFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..46157a71c01753e8c084653c0af457bf874160cb
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/RemovableDriveFinder.java
@@ -0,0 +1,10 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+interface RemovableDriveFinder {
+
+	List<File> findRemovableDrives() throws IOException;
+}
diff --git a/components/net/sf/briar/plugins/file/RemovableDriveFinderImpl.java b/components/net/sf/briar/plugins/file/RemovableDriveFinderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..26060e74fce8020756230b3ae33ad394be367919
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/RemovableDriveFinderImpl.java
@@ -0,0 +1,25 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.briar.util.OsUtils;
+
+class RemovableDriveFinderImpl implements RemovableDriveFinder {
+
+	private final LinuxRemovableDriveFinder linux =
+		new LinuxRemovableDriveFinder();
+	private final MacRemovableDriveFinder mac =
+		new MacRemovableDriveFinder();
+	private final WindowsRemovableDriveFinder windows =
+		new WindowsRemovableDriveFinder();
+
+	public List<File> findRemovableDrives() throws IOException {
+		if(OsUtils.isLinux()) return linux.findRemovableDrives();
+		else if(OsUtils.isMac()) return mac.findRemovableDrives();
+		else if(OsUtils.isWindows()) return windows.findRemovableDrives();
+		else return Collections.emptyList();
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a890d5b8c2567065ad6a4792cf06a84e2a1afbe
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
@@ -0,0 +1,36 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+class RemovableDrivePlugin extends FilePlugin {
+
+	private final RemovableDriveFinder finder;
+
+	RemovableDrivePlugin(RemovableDriveFinder finder) {
+		this.finder = finder;
+	}
+
+	@Override
+	protected File chooseOutputDirectory() {
+		try {
+			List<File> drives = finder.findRemovableDrives();
+			if(drives.isEmpty()) return null;
+			String[] paths = new String[drives.size()];
+			for(int i = 0; i < paths.length; i++) {
+				paths[i] = drives.get(i).getAbsolutePath();
+			}
+			int i = callback.showChoice("REMOVABLE_DRIVE_CHOOSE_DRIVE", paths);
+			if(i == -1) return null;
+			return drives.get(i);
+		} catch(IOException e) {
+			return null;
+		}
+	}
+
+	@Override
+	protected void writerFinished(File f) {
+		callback.showMessage("REMOVABLE_DRIVE_WRITE_FINISHED");
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/UnixRemovableDriveFinder.java b/components/net/sf/briar/plugins/file/UnixRemovableDriveFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..17a551bf7b614059b087efff30e659b2dd1b3380
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/UnixRemovableDriveFinder.java
@@ -0,0 +1,41 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+abstract class UnixRemovableDriveFinder implements RemovableDriveFinder {
+
+	protected abstract String getMountCommand();
+
+	protected abstract String parseMountPoint(String line);
+
+	protected abstract boolean isRemovableDriveMountPoint(String path);
+
+	public List<File> findRemovableDrives() throws IOException {
+		List<File> drives = new ArrayList<File>();
+		Process p = new ProcessBuilder(getMountCommand()).start();
+		Scanner s = new Scanner(p.getInputStream(), "UTF-8");
+		try {
+			while(s.hasNextLine()) {
+				String line = s.nextLine();
+				String[] tokens = line.split(" ");
+				if(tokens.length < 3) continue;
+				// The general format is "/dev/foo on /bar/baz ..."
+				if(tokens[0].startsWith("/dev/") && tokens[1].equals("on")) {
+					// The path may contain spaces so we can't use tokens[2]
+					String path = parseMountPoint(line);
+					if(isRemovableDriveMountPoint(path)) {
+						File f = new File(path);
+						if(f.exists() && f.isDirectory()) drives.add(f);
+					}
+				}
+			}
+		} finally {
+			s.close();
+		}
+		return drives;
+	}
+}
diff --git a/components/net/sf/briar/plugins/file/WindowsRemovableDriveFinder.java b/components/net/sf/briar/plugins/file/WindowsRemovableDriveFinder.java
new file mode 100644
index 0000000000000000000000000000000000000000..78c2dfab14684c668fd10b954033eaaf4e85d32e
--- /dev/null
+++ b/components/net/sf/briar/plugins/file/WindowsRemovableDriveFinder.java
@@ -0,0 +1,30 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.sun.jna.platform.win32.Kernel32;
+
+class WindowsRemovableDriveFinder implements RemovableDriveFinder {
+
+	// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364939.aspx
+	private static final int DRIVE_REMOVABLE = 2;
+
+	public List<File> findRemovableDrives() throws IOException {
+		File[] roots = File.listRoots();
+		if(roots == null) throw new IOException();
+		List<File> drives = new ArrayList<File>();
+		for(File root : roots) {
+			try {
+				int type = Kernel32.INSTANCE.GetDriveType(root.getPath());
+				if(type == DRIVE_REMOVABLE) drives.add(root);
+			} catch(RuntimeException e) {
+				throw new IOException(e.getMessage());
+			}
+		}
+		return drives;
+	}
+
+}
diff --git a/test/net/sf/briar/plugins/file/TestFilePlugin.java b/test/net/sf/briar/plugins/file/TestFilePlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..b3a065a6cbbd30b9d0742512836c657fa630c756
--- /dev/null
+++ b/test/net/sf/briar/plugins/file/TestFilePlugin.java
@@ -0,0 +1,29 @@
+package net.sf.briar.plugins.file;
+
+import java.io.File;
+
+public class TestFilePlugin extends FilePlugin {
+
+	private final File outputDir;
+	private final long capacity;
+
+	public TestFilePlugin(File outputDir, long capacity) {
+		this.outputDir = outputDir;
+		this.capacity = capacity;
+	}
+
+	@Override
+	protected File chooseOutputDirectory() {
+		return outputDir;
+	}
+
+	@Override
+	protected void writerFinished(File f) {
+		// Nothing to do
+	}
+
+	@Override
+	protected long getCapacity(String path) {
+		return capacity;
+	}
+}