Add MailboxFileManager for downloads (uploads to be added later).

This commit is contained in:
akwizgran
2022-06-02 14:24:54 +01:00
parent 6aa24af94c
commit b128220be3
11 changed files with 470 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
package org.briarproject.bramble.api.mailbox;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Annotation for injecting the {@link File directory} where the Mailbox plugin
* should store its state.
*/
@Qualifier
@Target({FIELD, METHOD, PARAMETER})
@Retention(RUNTIME)
public @interface MailboxDirectory {
}

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.File;
import java.io.IOException;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
interface MailboxFileManager {
/**
* Creates an empty file for storing a download.
*/
File createTempFileForDownload() throws IOException;
/**
* Handles a file that has been downloaded. The file should be created
* with {@link #createTempFileForDownload()}.
*/
void handleDownloadedFile(File f);
}

View File

@@ -0,0 +1,174 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.mailbox.MailboxDirectory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class MailboxFileManagerImpl implements MailboxFileManager, EventListener {
private static final Logger LOG =
getLogger(MailboxFileManagerImpl.class.getName());
// Package access for testing
static final String DOWNLOAD_DIR_NAME = "downloads";
private final Executor ioExecutor;
private final PluginManager pluginManager;
private final ConnectionManager connectionManager;
private final LifecycleManager lifecycleManager;
private final File mailboxDir;
private final EventBus eventBus;
private final CountDownLatch orphanLatch = new CountDownLatch(1);
@Inject
MailboxFileManagerImpl(@IoExecutor Executor ioExecutor,
PluginManager pluginManager,
ConnectionManager connectionManager,
LifecycleManager lifecycleManager,
@MailboxDirectory File mailboxDir,
EventBus eventBus) {
this.ioExecutor = ioExecutor;
this.pluginManager = pluginManager;
this.connectionManager = connectionManager;
this.lifecycleManager = lifecycleManager;
this.mailboxDir = mailboxDir;
this.eventBus = eventBus;
}
@Override
public File createTempFileForDownload() throws IOException {
// Wait for orphaned files to be handled before creating new files
try {
orphanLatch.await();
} catch (InterruptedException e) {
throw new IOException(e);
}
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
return File.createTempFile("mailbox", ".tmp", downloadDir);
}
private File createDirectoryIfNeeded(String name) throws IOException {
File dir = new File(mailboxDir, name);
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
if (!dir.isDirectory()) {
throw new IOException("Failed to create directory '" + name + "'");
}
return dir;
}
@Override
public void handleDownloadedFile(File f) {
// We shouldn't reach this point until the plugin has been started
SimplexPlugin plugin =
(SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID));
TransportProperties p = new TransportProperties();
p.put(PROP_PATH, f.getAbsolutePath());
TransportConnectionReader reader = plugin.createReader(p);
if (reader == null) {
LOG.warning("Failed to create reader for downloaded file");
return;
}
TransportConnectionReader decorated = new MailboxFileReader(reader, f);
LOG.info("Reading downloaded file");
connectionManager.manageIncomingConnection(ID, decorated,
exception -> isHandlingComplete(exception, true));
}
private boolean isHandlingComplete(boolean exception, boolean recognised) {
// If we've successfully read the file then we're done
if (!exception && recognised) return true;
// If the app is shutting down we may get spurious IO exceptions
// due to executors being shut down. Leave the file in the download
// directory and we'll try to read it again at the next startup
return !lifecycleManager.getLifecycleState().isAfter(RUNNING);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof TransportActiveEvent) {
TransportActiveEvent t = (TransportActiveEvent) e;
if (t.getTransportId().equals(ID)) {
ioExecutor.execute(this::handleOrphanedFiles);
eventBus.removeListener(this);
}
}
}
/**
* This method is called at startup, as soon as the plugin is started, to
* handle any files that were left in the download directory at the last
* shutdown.
*/
@IoExecutor
private void handleOrphanedFiles() {
try {
File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME);
File[] orphans = downloadDir.listFiles();
// Now that we've got the list of orphans, new files can be created
orphanLatch.countDown();
if (orphans != null) for (File f : orphans) handleDownloadedFile(f);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
private class MailboxFileReader implements TransportConnectionReader {
private final TransportConnectionReader delegate;
private final File file;
private MailboxFileReader(TransportConnectionReader delegate,
File file) {
this.delegate = delegate;
this.file = file;
}
@Override
public InputStream getInputStream() throws IOException {
return delegate.getInputStream();
}
@Override
public void dispose(boolean exception, boolean recognised)
throws IOException {
delegate.dispose(exception, recognised);
if (isHandlingComplete(exception, recognised)) {
LOG.info("Deleting downloaded file");
if (!file.delete()) {
LOG.warning("Failed to delete downloaded file");
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.mailbox.MailboxManager;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
@@ -34,6 +35,8 @@ public class MailboxModule {
MailboxUpdateValidator mailboxUpdateValidator;
@Inject
MailboxUpdateManager mailboxUpdateManager;
@Inject
MailboxFileManager mailboxFileManager;
}
@Provides
@@ -101,4 +104,14 @@ public class MailboxModule {
}
return mailboxUpdateManager;
}
@Provides
@Singleton
MailboxFileManager provideMailboxFileManager(FeatureFlags featureFlags,
EventBus eventBus, MailboxFileManagerImpl mailboxFileManager) {
if (featureFlags.shouldEnableMailbox()) {
eventBus.addListener(mailboxFileManager);
}
return mailboxFileManager;
}
}

View File

@@ -0,0 +1,194 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.connection.ConnectionManager.TagController;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
import org.briarproject.bramble.api.plugin.PluginManager;
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.briarproject.bramble.test.RunAction;
import org.jmock.Expectations;
import org.jmock.lib.action.DoAllAction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING;
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.DOWNLOAD_DIR_NAME;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MailboxFileManagerImplTest extends BrambleMockTestCase {
private final Executor ioExecutor = context.mock(Executor.class);
private final PluginManager pluginManager =
context.mock(PluginManager.class);
private final ConnectionManager connectionManager =
context.mock(ConnectionManager.class);
private final LifecycleManager lifecycleManager =
context.mock(LifecycleManager.class);
private final EventBus eventBus = context.mock(EventBus.class);
private final SimplexPlugin plugin = context.mock(SimplexPlugin.class);
private final TransportConnectionReader transportConnectionReader =
context.mock(TransportConnectionReader.class);
private File mailboxDir;
private MailboxFileManagerImpl manager;
@Before
public void setUp() {
mailboxDir = getTestDirectory();
manager = new MailboxFileManagerImpl(ioExecutor, pluginManager,
connectionManager, lifecycleManager, mailboxDir, eventBus);
}
@After
public void tearDown() {
deleteTestDirectory(mailboxDir);
}
@Test
public void testHandlesOrphanedFilesAtStartup() throws Exception {
// Create an orphaned file, left behind at the previous shutdown
File downloadDir = new File(mailboxDir, DOWNLOAD_DIR_NAME);
//noinspection ResultOfMethodCallIgnored
downloadDir.mkdirs();
File orphan = new File(downloadDir, "orphan");
assertTrue(orphan.createNewFile());
TransportProperties props = new TransportProperties();
props.put(PROP_PATH, orphan.getAbsolutePath());
// When the plugin becomes active the orphaned file should be handled
context.checking(new Expectations() {{
oneOf(ioExecutor).execute(with(any(Runnable.class)));
will(new RunAction());
oneOf(eventBus).removeListener(manager);
oneOf(pluginManager).getPlugin(ID);
will(returnValue(plugin));
oneOf(plugin).createReader(props);
will(returnValue(transportConnectionReader));
oneOf(connectionManager).manageIncomingConnection(with(ID),
with(any(TransportConnectionReader.class)),
with(any(TagController.class)));
}});
manager.eventOccurred(new TransportActiveEvent(ID));
}
@Test
public void testDeletesFileWhenReadSucceeds() throws Exception {
expectCheckForOrphans();
manager.eventOccurred(new TransportActiveEvent(ID));
File f = manager.createTempFileForDownload();
AtomicReference<TransportConnectionReader> reader =
new AtomicReference<>(null);
AtomicReference<TagController> controller = new AtomicReference<>(null);
expectPassFileToConnectionManager(f, reader, controller);
manager.handleDownloadedFile(f);
// The read is successful, so the tag controller should allow the tag
// to be marked as read and the reader should delete the file
context.checking(new Expectations() {{
oneOf(transportConnectionReader).dispose(false, true);
}});
assertTrue(controller.get().shouldMarkTagAsRecognised(false));
reader.get().dispose(false, true);
assertFalse(f.exists());
}
@Test
public void testDeletesFileWhenTagIsNotRecognised() throws Exception {
testDeletesFile(false, RUNNING, false);
}
@Test
public void testDeletesFileWhenReadFails() throws Exception {
testDeletesFile(true, RUNNING, false);
}
@Test
public void testDoesNotDeleteFileWhenTagIsNotRecognisedAtShutdown()
throws Exception {
testDeletesFile(false, STOPPING, true);
}
@Test
public void testDoesNotDeleteFileWhenReadFailsAtShutdown()
throws Exception {
testDeletesFile(true, STOPPING, true);
}
private void testDeletesFile(boolean recognised, LifecycleState state,
boolean fileExists) throws Exception {
expectCheckForOrphans();
manager.eventOccurred(new TransportActiveEvent(ID));
File f = manager.createTempFileForDownload();
AtomicReference<TransportConnectionReader> reader =
new AtomicReference<>(null);
AtomicReference<TagController> controller = new AtomicReference<>(null);
expectPassFileToConnectionManager(f, reader, controller);
manager.handleDownloadedFile(f);
context.checking(new Expectations() {{
oneOf(transportConnectionReader).dispose(true, recognised);
oneOf(lifecycleManager).getLifecycleState();
will(returnValue(state));
}});
reader.get().dispose(true, recognised);
assertEquals(fileExists, f.exists());
}
private void expectCheckForOrphans() {
context.checking(new Expectations() {{
oneOf(ioExecutor).execute(with(any(Runnable.class)));
will(new RunAction());
oneOf(eventBus).removeListener(manager);
}});
}
private void expectPassFileToConnectionManager(File f,
AtomicReference<TransportConnectionReader> reader,
AtomicReference<TagController> controller) {
TransportProperties props = new TransportProperties();
props.put(PROP_PATH, f.getAbsolutePath());
context.checking(new Expectations() {{
oneOf(pluginManager).getPlugin(ID);
will(returnValue(plugin));
oneOf(plugin).createReader(props);
will(returnValue(transportConnectionReader));
oneOf(connectionManager).manageIncomingConnection(with(ID),
with(any(TransportConnectionReader.class)),
with(any(TagController.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(reader,
TransportConnectionReader.class, 1),
new CaptureArgumentAction<>(controller,
TagController.class, 2)
));
}});
}
}

View File

@@ -13,6 +13,7 @@ import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestFeatureFlagModule;
import org.briarproject.bramble.test.TestMailboxDirectoryModule;
import org.briarproject.bramble.test.TestSecureRandomModule;
import javax.inject.Singleton;
@@ -27,6 +28,7 @@ import dagger.Component;
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
TestFeatureFlagModule.class,
TestMailboxDirectoryModule.class,
RemovableDriveIntegrationTestModule.class,
RemovableDriveModule.class,
TestSecureRandomModule.class,

View File

@@ -13,6 +13,7 @@ import dagger.Module;
DefaultWakefulIoExecutorModule.class,
TestDatabaseConfigModule.class,
TestFeatureFlagModule.class,
TestMailboxDirectoryModule.class,
TestPluginConfigModule.class,
TestSecureRandomModule.class,
TimeTravelModule.class

View File

@@ -0,0 +1,18 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.mailbox.MailboxDirectory;
import java.io.File;
import dagger.Module;
import dagger.Provides;
@Module
public class TestMailboxDirectoryModule {
@Provides
@MailboxDirectory
File provideMailboxDirectory() {
return new File("mailbox");
}
}

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.db.DatabaseConfig;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.mailbox.MailboxDirectory;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
@@ -157,6 +158,13 @@ public class AppModule {
return new AndroidDatabaseConfig(dbDir, keyDir, keyStrengthener);
}
@Provides
@Singleton
@MailboxDirectory
File provideMailboxDirectory(Application app) {
return app.getDir("mailbox", MODE_PRIVATE);
}
@Provides
@Singleton
@TorDirectory

View File

@@ -6,6 +6,7 @@ import dagger.Provides
import org.briarproject.bramble.account.AccountModule
import org.briarproject.bramble.api.FeatureFlags
import org.briarproject.bramble.api.db.DatabaseConfig
import org.briarproject.bramble.api.mailbox.MailboxDirectory
import org.briarproject.bramble.api.plugin.PluginConfig
import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT
import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_PORT
@@ -71,6 +72,12 @@ internal class HeadlessModule(private val appDir: File) {
return HeadlessDatabaseConfig(dbDir, keyDir)
}
@Provides
@MailboxDirectory
internal fun provideMailboxDirectory(): File {
return File(appDir, "mailbox")
}
@Provides
@TorDirectory
internal fun provideTorDirectory(): File {

View File

@@ -5,6 +5,7 @@ import dagger.Module
import dagger.Provides
import org.briarproject.bramble.account.AccountModule
import org.briarproject.bramble.api.db.DatabaseConfig
import org.briarproject.bramble.api.mailbox.MailboxDirectory
import org.briarproject.bramble.api.plugin.PluginConfig
import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT
import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_PORT
@@ -68,6 +69,12 @@ internal class HeadlessTestModule(private val appDir: File) {
return HeadlessDatabaseConfig(dbDir, keyDir)
}
@Provides
@MailboxDirectory
internal fun provideMailboxDirectory(): File {
return File(appDir, "mailbox")
}
@Provides
@TorSocksPort
internal fun provideTorSocksPort(): Int = DEFAULT_SOCKS_PORT