1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2025-01-09 03:31:46 +00:00

Implement import/export of the index database (fixes #83) (#84)

* Add export of index database

* Add import of index database

* Shutdown service before export and restart it afterwards.

* Do not import database if it doesn't exist on sdcard

* Do not attempt to delete the database export directory on export if it does not exist.

* Return to MainActivity after successful export

* Import/Export using an AsyncTask

* Fix compatibility with Android 5.x
This commit is contained in:
Catfriend1 2018-10-10 01:06:25 +02:00 committed by GitHub
parent 79d0d7cc4c
commit 12bc08c6dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 38 deletions

View file

@ -536,16 +536,9 @@ public class SettingsActivity extends SyncthingActivity {
new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_export)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
if (mSyncthingService.exportConfig()) {
Toast.makeText(getActivity(),
getString(R.string.config_export_successful,
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(),
getString(R.string.config_export_failed),
Toast.LENGTH_LONG).show();
}
})
new ExportConfigTask((SettingsActivity) getActivity(), mSyncthingService)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
})
.setNegativeButton(android.R.string.no, null)
.show();
return true;
@ -553,22 +546,9 @@ public class SettingsActivity extends SyncthingActivity {
new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_confirm_import)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
// Shutdown syncthing, import config, if run conditions applied restart syncthing.
if (!mSyncthingService.importConfig()) {
Toast.makeText(getActivity(),
getString(R.string.config_import_failed,
Constants.EXPORT_PATH), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(getActivity(),
getString(R.string.config_imported_successful),
Toast.LENGTH_SHORT).show();
// We don't have to send the config via REST on leaving activity.
mPendingConfig = false;
// We have to evaluate run conditions, they may have changed by the imported prefs.
mPendingRunConditions = true;
getActivity().finish();
})
new ImportConfigTask(this, mSyncthingService)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
})
.setNegativeButton(android.R.string.no, null)
.show();
return true;
@ -658,6 +638,107 @@ public class SettingsActivity extends SyncthingActivity {
}
}
/**
* Performs export of settings, config and database in the background.
*/
private static class ExportConfigTask extends AsyncTask<Void, String, Void> {
private WeakReference<SettingsActivity> refSettingsActivity;
private WeakReference<SyncthingService> refSyncthingService;
Boolean actionSucceeded = false;
ExportConfigTask(SettingsActivity context, SyncthingService service) {
refSettingsActivity = new WeakReference<>(context);
refSyncthingService = new WeakReference<>(service);
}
@Override
protected Void doInBackground(Void... voids) {
SyncthingService syncthingService = refSyncthingService.get();
if (syncthingService == null) {
cancel(true);
return null;
}
actionSucceeded = syncthingService.exportConfig();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// Get a reference to the activity if it is still there.
SettingsActivity settingsActivity = refSettingsActivity.get();
if (settingsActivity == null) {
return;
}
if (!actionSucceeded) {
Toast.makeText(settingsActivity,
settingsActivity.getString(R.string.config_export_failed),
Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(settingsActivity,
settingsActivity.getString(R.string.config_export_successful,
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
settingsActivity.finish();
}
}
/**
* Performs import of settings, config and database in the background.
*/
private static class ImportConfigTask extends AsyncTask<Void, String, Void> {
private WeakReference<SettingsFragment> refSettingsFragment;
private WeakReference<SyncthingService> refSyncthingService;
Boolean actionSucceeded = false;
ImportConfigTask(SettingsFragment context, SyncthingService service) {
refSettingsFragment = new WeakReference<>(context);
refSyncthingService = new WeakReference<>(service);
}
@Override
protected Void doInBackground(Void... voids) {
SyncthingService syncthingService = refSyncthingService.get();
if (syncthingService == null) {
cancel(true);
return null;
}
actionSucceeded = syncthingService.importConfig();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// Get a reference to the activity if it is still there.
SettingsFragment settingsFragment = refSettingsFragment.get();
if (settingsFragment == null) {
return;
}
settingsFragment.afterConfigImport(actionSucceeded);
}
}
/**
* Calley by {@link #ImportConfigTask} after config import.
*/
private void afterConfigImport(Boolean actionSucceeded) {
if (!actionSucceeded) {
Toast.makeText(getActivity(),
getString(R.string.config_import_failed,
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(getActivity(),
getString(R.string.config_imported_successful,
Constants.EXPORT_PATH_OBJ), Toast.LENGTH_LONG).show();
// We don't have to send the config via REST on leaving activity.
mPendingConfig = false;
// We have to evaluate run conditions, they may have changed by the imported prefs.
mPendingRunConditions = true;
getActivity().finish();
}
/**
* Handles a new user input for the SOCKS proxy preference.
* Returns if the changed setting requires a restart.

View file

@ -68,8 +68,9 @@ public class Constants {
/**
* Directory where config is exported to and imported from.
*/
public static final File EXPORT_PATH =
new File(Environment.getExternalStorageDirectory(), "backups/syncthing");
public static final String EXPORT_PATH = Environment.getExternalStorageDirectory() + "/backups/syncthing";
public static final File EXPORT_PATH_OBJ = new File(EXPORT_PATH);
/**
* File in the config folder that contains configuration.
@ -107,6 +108,11 @@ public class Constants {
return new File(context.getFilesDir(), PRIVATE_KEY_FILE);
}
/**
* Name of the folder containing the index database.
*/
static final String INDEX_DB_FOLDER = "index-v0.14.0.db";
/**
* Name of the public HTTPS CA file in the data directory.
*/

View file

@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
import android.content.SharedPreferences;
import android.Manifest;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.Nullable;
@ -21,6 +22,7 @@ import com.nutomic.syncthingandroid.SyncthingApp;
import com.nutomic.syncthingandroid.http.PollWebGuiAvailableTask;
import com.nutomic.syncthingandroid.model.Folder;
import com.nutomic.syncthingandroid.util.ConfigXml;
import com.nutomic.syncthingandroid.util.FileUtils;
import java.io.File;
import java.io.FileInputStream;
@ -30,6 +32,8 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@ -664,20 +668,28 @@ public class SyncthingService extends Service {
/**
* Exports the local config and keys to {@link Constants#EXPORT_PATH}.
*
* Test with Android Virtual Device using emulator.
* cls & adb shell su 0 "ls -a -l -R /data/data/com.github.catfriend1.syncthingandroid.debug/files; echo === SDCARD ===; ls -a -l -R /storage/emulated/0/backups/syncthing"
*
*/
public boolean exportConfig() {
Boolean failSuccess = true;
Log.v(TAG, "exportConfig BEGIN");
// Shutdown synchronously.
shutdown(State.DISABLED, () -> {
});
// Copy config, privateKey and/or publicKey to export path.
Constants.EXPORT_PATH.mkdirs();
Constants.EXPORT_PATH_OBJ.mkdirs();
try {
Files.copy(Constants.getConfigFile(this),
new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE));
new File(Constants.EXPORT_PATH_OBJ, Constants.CONFIG_FILE));
Files.copy(Constants.getPrivateKeyFile(this),
new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE));
new File(Constants.EXPORT_PATH_OBJ, Constants.PRIVATE_KEY_FILE));
Files.copy(Constants.getPublicKeyFile(this),
new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE));
new File(Constants.EXPORT_PATH_OBJ, Constants.PUBLIC_KEY_FILE));
} catch (IOException e) {
Log.w(TAG, "Failed to export config", e);
failSuccess = false;
@ -688,7 +700,7 @@ public class SyncthingService extends Service {
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
file = new File(Constants.EXPORT_PATH_OBJ, Constants.SHARED_PREFS_EXPORT_FILE);
fileOutputStream = new FileOutputStream(file);
if (!file.exists()) {
file.createNewFile();
@ -712,12 +724,49 @@ public class SyncthingService extends Service {
Log.e(TAG, "exportConfig: Failed to export SharedPreferences #2", e);
}
}
/**
* java.nio.file library is available since API level 26, see
* https://developer.android.com/reference/java/nio/file/package-summary
*/
if (Build.VERSION.SDK_INT >= 26) {
Log.v(TAG, "exportConfig: Exporting index database");
Path databaseSourcePath = Paths.get(this.getFilesDir() + "/" + Constants.INDEX_DB_FOLDER);
Path databaseExportPath = Paths.get(Constants.EXPORT_PATH + "/" + Constants.INDEX_DB_FOLDER);
if (java.nio.file.Files.exists(databaseExportPath)) {
try {
FileUtils.deleteDirectoryRecursively(databaseExportPath);
} catch (IOException e) {
Log.e(TAG, "Failed to delete directory '" + databaseExportPath + "'" + e);
}
}
try {
java.nio.file.Files.walk(databaseSourcePath).forEach(source -> {
try {
java.nio.file.Files.copy(source, databaseExportPath.resolve(databaseSourcePath.relativize(source)));
} catch (IOException e) {
Log.e(TAG, "Failed to copy file '" + source + "' to '" + databaseExportPath + "'");
}
});
} catch (IOException e) {
Log.e(TAG, "Failed to copy directory '" + databaseSourcePath + "' to '" + databaseExportPath + "'");
}
}
Log.v(TAG, "exportConfig END");
// Start syncthing after export if run conditions apply.
if (mLastDeterminedShouldRun) {
launchStartupTask(SyncthingRunnable.Command.main);
}
return failSuccess;
}
/**
* Imports config and keys from {@link Constants#EXPORT_PATH}.
*
* Test with Android Virtual Device using emulator.
* cls & adb shell su 0 "ls -a -l -R /data/data/com.github.catfriend1.syncthingandroid.debug/files; echo === SDCARD ===; ls -a -l -R /storage/emulated/0/backups/syncthing"
*
* @return True if the import was successful, false otherwise (eg if files aren't found).
*/
public boolean importConfig() {
@ -730,9 +779,9 @@ public class SyncthingService extends Service {
// Import config, privateKey and/or publicKey.
try {
File config = new File(Constants.EXPORT_PATH, Constants.CONFIG_FILE);
File privateKey = new File(Constants.EXPORT_PATH, Constants.PRIVATE_KEY_FILE);
File publicKey = new File(Constants.EXPORT_PATH, Constants.PUBLIC_KEY_FILE);
File config = new File(Constants.EXPORT_PATH_OBJ, Constants.CONFIG_FILE);
File privateKey = new File(Constants.EXPORT_PATH_OBJ, Constants.PRIVATE_KEY_FILE);
File publicKey = new File(Constants.EXPORT_PATH_OBJ, Constants.PUBLIC_KEY_FILE);
// Check if necessary files for import are available.
if (config.exists() && privateKey.exists() && publicKey.exists()) {
@ -754,7 +803,7 @@ public class SyncthingService extends Service {
ObjectInputStream objectInputStream = null;
Map<String, Object> sharedPrefsMap = null;
try {
file = new File(Constants.EXPORT_PATH, Constants.SHARED_PREFS_EXPORT_FILE);
file = new File(Constants.EXPORT_PATH_OBJ, Constants.SHARED_PREFS_EXPORT_FILE);
if (file.exists()) {
// Read, deserialize shared preferences.
fileInputStream = new FileInputStream(file);
@ -831,7 +880,36 @@ public class SyncthingService extends Service {
}
}
// Start syncthing after successful import if run conditions apply.
/**
* java.nio.file library is available since API level 26, see
* https://developer.android.com/reference/java/nio/file/package-summary
*/
if (Build.VERSION.SDK_INT >= 26) {
Path databaseImportPath = Paths.get(Constants.EXPORT_PATH + "/" + Constants.INDEX_DB_FOLDER);
if (java.nio.file.Files.exists(databaseImportPath)) {
Log.v(TAG, "importConfig: Importing index database");
Path databaseTargetPath = Paths.get(this.getFilesDir() + "/" + Constants.INDEX_DB_FOLDER);
try {
FileUtils.deleteDirectoryRecursively(databaseTargetPath);
} catch (IOException e) {
Log.e(TAG, "Failed to delete directory '" + databaseTargetPath + "'" + e);
}
try {
java.nio.file.Files.walk(databaseImportPath).forEach(source -> {
try {
java.nio.file.Files.copy(source, databaseTargetPath.resolve(databaseImportPath.relativize(source)));
} catch (IOException e) {
Log.e(TAG, "Failed to copy file '" + source + "' to '" + databaseTargetPath + "'");
}
});
} catch (IOException e) {
Log.e(TAG, "Failed to copy directory '" + databaseImportPath + "' to '" + databaseTargetPath + "'");
}
}
}
Log.v(TAG, "importConfig END");
// Start syncthing after import if run conditions apply.
if (mLastDeterminedShouldRun) {
launchStartupTask(SyncthingRunnable.Command.main);
}

View file

@ -12,10 +12,12 @@ import android.support.annotation.Nullable;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
/**
* Utils for dealing with Storage Access Framework URIs.
@ -204,4 +206,17 @@ public class FileUtils {
}
return path;
}
/**
* Deletes a directory recursively.
* java.nio.file library is available since API level 26, see
* https://developer.android.com/reference/java/nio/file/package-summary
*/
@TargetApi(26)
public static void deleteDirectoryRecursively(java.nio.file.Path pathToDelete) throws IOException {
java.nio.file.Files.walk(pathToDelete)
.sorted(Comparator.reverseOrder())
.map(java.nio.file.Path::toFile)
.forEach(File::delete);
}
}