diff --git a/apps/mobile/app/common/filesystem/utils.js b/apps/mobile/app/common/filesystem/utils.js
index e4de8b857..11b92c6e1 100644
--- a/apps/mobile/app/common/filesystem/utils.js
+++ b/apps/mobile/app/common/filesystem/utils.js
@@ -66,9 +66,12 @@ export function cancelable(operation) {
}
export function copyFileAsync(source, dest) {
- return new Promise((resolve) => {
+ return new Promise((resolve, reject) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
- console.log(e, r);
+ if (e) {
+ reject(e);
+ return;
+ }
resolve();
});
});
diff --git a/apps/mobile/app/components/sheets/migrate/index.tsx b/apps/mobile/app/components/sheets/migrate/index.tsx
index f24c8d724..bcb584d98 100644
--- a/apps/mobile/app/components/sheets/migrate/index.tsx
+++ b/apps/mobile/app/components/sheets/migrate/index.tsx
@@ -92,16 +92,16 @@ export default function Migrate() {
try {
setLoading(true);
await sleep(1000);
- const backupSaved = await BackupService.run(false, "local");
- if (!backupSaved) {
- ToastEvent.show({
- heading: "Migration failed",
- message: "You must download a backup of your data before migrating.",
- context: "local"
- });
- setLoading(false);
- return;
- }
+ // const backupSaved = await BackupService.run(false, "local");
+ // if (!backupSaved) {
+ // ToastEvent.show({
+ // heading: "Migration failed",
+ // message: "You must download a backup of your data before migrating.",
+ // context: "local"
+ // });
+ // setLoading(false);
+ // return;
+ // }
await db.migrations?.migrate();
eSendEvent(eCloseSheet);
await sleep(500);
diff --git a/apps/mobile/app/components/sheets/restore-data/index.js b/apps/mobile/app/components/sheets/restore-data/index.js
index 86d3fb88c..94ee21375 100644
--- a/apps/mobile/app/components/sheets/restore-data/index.js
+++ b/apps/mobile/app/components/sheets/restore-data/index.js
@@ -22,7 +22,9 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import RNFetchBlob from "react-native-blob-util";
-import DocumentPicker from "react-native-document-picker";
+import DocumentPicker, {
+ DocumentPickerResponse
+} from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
@@ -45,6 +47,8 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
+import { unzip } from "react-native-zip-archive";
+import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
@@ -129,75 +133,71 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
useEffect(() => {
setTimeout(() => {
checkBackups();
- }, 300);
+ }, 1000);
}, []);
const restore = async (item) => {
if (restoring) {
return;
}
- try {
- setRestoring(true);
- let prefix = Platform.OS === "ios" ? "" : "file:/";
- let backup;
- if (Platform.OS === "android") {
- backup = await ScopedStorage.readFile(item.uri, "utf8");
- } else {
- backup = await RNFetchBlob.fs.readFile(prefix + item.path, "utf8");
- }
- backup = JSON.parse(backup);
- if (backup.data.iv && backup.data.salt) {
- withPassword(
- async (value) => {
- try {
- await restoreBackup(backup, value);
- close();
- setRestoring(false);
- return true;
- } catch (e) {
- backupError(e);
- return false;
- }
- },
- () => {
- setRestoring(false);
+ try {
+ const file = Platform.OS === "ios" ? item.path : item.uri;
+ console.log(file);
+ if (file.endsWith(".nnbackupz")) {
+ setRestoring(true);
+
+ if (Platform.OS === "android") {
+ const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
+ if (await RNFetchBlob.fs.exists(cacheFile)) {
+ await RNFetchBlob.fs.unlink(cacheFile);
}
- );
- } else {
- await restoreBackup(backup);
- close();
+ await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
+ console.log("copying");
+ await copyFileAsync(file, cacheFile);
+ console.log("copied");
+ await restoreFromZip(cacheFile);
+ } else {
+ await restoreFromZip(file, false);
+ }
+ } else if (file.endsWith(".nnbackup")) {
+ let backup;
+ if (Platform.OS === "android") {
+ backup = await ScopedStorage.readFile(file, "utf8");
+ } else {
+ backup = await RNFetchBlob.fs.readFile(file, "utf8");
+ }
+ await restoreFromNNBackup(JSON.parse(backup));
}
} catch (e) {
+ console.log("error", e);
setRestoring(false);
backupError(e);
}
};
- const withPassword = (onsubmit, onclose = () => {}) => {
- presentDialog({
- context: "local",
- title: "Encrypted backup",
- input: true,
- inputPlaceholder: "Password",
- paragraph: "Please enter password of this backup file to restore it",
- positiveText: "Restore",
- secureTextEntry: true,
- onClose: onclose,
- negativeText: "Cancel",
- positivePress: async (password) => {
- try {
- return await onsubmit(password);
- } catch (e) {
- ToastEvent.show({
- heading: "Failed to backup data",
- message: e.message,
- type: "error",
- context: "global"
- });
- return false;
+ const withPassword = () => {
+ return new Promise((resolve) => {
+ let resolved = false;
+ presentDialog({
+ context: "local",
+ title: "Encrypted backup",
+ input: true,
+ inputPlaceholder: "Password",
+ paragraph: "Please enter password of this backup file to restore it",
+ positiveText: "Restore",
+ secureTextEntry: true,
+ onClose: () => {
+ if (resolved) return;
+ resolve(undefined);
+ },
+ negativeText: "Cancel",
+ positivePress: async (password) => {
+ resolve(password);
+ resolved = true;
+ return true;
}
- }
+ });
});
};
@@ -217,21 +217,164 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
let path = await storage.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
- files = files.sort(function (a, b) {
- let timeA = a.lastModified;
- let timeB = b.lastModified;
- return timeB - timeA;
- });
+ files = files
+ .filter((file) => {
+ const name = Platform.OS === "android" ? file.name : file.filename;
+ return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
+ })
+ .sort(function (a, b) {
+ let timeA = a.lastModified;
+ let timeB = b.lastModified;
+ return timeB - timeA;
+ });
setFiles(files);
- setTimeout(() => {
- setLoading(false);
- }, 1000);
+ setLoading(false);
} catch (e) {
console.log(e);
setLoading(false);
}
};
+ const restoreBackup = async (backup, password) => {
+ await db.backup.import(backup, password);
+ await db.initCollections();
+ initialize();
+ ToastEvent.show({
+ heading: "Backup restored successfully.",
+ type: "success",
+ context: "global"
+ });
+ return true;
+ };
+
+ const backupError = (e) => {
+ ToastEvent.show({
+ heading: "Restore failed",
+ message:
+ e.message ||
+ "The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
+ type: "error",
+ context: "local"
+ });
+ };
+
+ /**
+ *
+ * @param {string} file
+ */
+ async function restoreFromZip(file, remove) {
+ try {
+ const zipOutputFolder = `${cacheDir}/backup_extracted`;
+ if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
+ await RNFetchBlob.fs.unlink(zipOutputFolder);
+ await RNFetchBlob.fs.mkdir(zipOutputFolder);
+ }
+ await unzip(file, zipOutputFolder);
+ console.log("Unzipped files successfully to", zipOutputFolder);
+
+ const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
+
+ if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
+ throw new Error("Backup file is invalid");
+ }
+
+ let password;
+
+ console.log(`Found ${backupFiles?.length} files to restore from backup`);
+ for (const path of backupFiles) {
+ if (path === ".nnbackup") continue;
+ const filePath = `${zipOutputFolder}/${path}`;
+ const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
+ const parsed = JSON.parse(data);
+
+ if (parsed.encrypted && !password) {
+ console.log("Backup is encrypted...", "requesting password");
+ password = await withPassword();
+ if (!password) throw new Error("Failed to decrypt backup");
+ }
+ await db.backup.import(parsed, password);
+ console.log("Imported", path);
+ }
+ // Remove files from cache
+ RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
+ if (remove) {
+ RNFetchBlob.fs.unlink(file).catch(console.log);
+ }
+
+ await db.initCollections();
+ initialize();
+ setRestoring(false);
+ close();
+ ToastEvent.show({
+ heading: "Backup restored successfully.",
+ type: "success",
+ context: "global"
+ });
+ } catch (e) {
+ backupError(e);
+ setRestoring(false);
+ }
+ }
+
+ /**
+ *
+ * @param {string} file
+ */
+ async function restoreFromNNBackup(backup) {
+ try {
+ if (backup.data.iv && backup.data.salt) {
+ const password = await withPassword();
+ if (password) {
+ try {
+ await restoreBackup(backup, password);
+ close();
+ setRestoring(false);
+ } catch (e) {
+ setRestoring(false);
+ backupError(e);
+ }
+ } else {
+ setRestoring(false);
+ }
+ } else {
+ await restoreBackup(backup);
+ setRestoring(false);
+ close();
+ }
+ } catch (e) {
+ setRestoring(false);
+ backupError(e);
+ }
+ }
+
+ const button = {
+ title: "Restore from files",
+ onPress: async () => {
+ if (restoring) {
+ return;
+ }
+ try {
+ const file = await DocumentPicker.pickSingle({
+ copyTo: "cachesDirectory"
+ });
+
+ if (file.name.endsWith(".nnbackupz")) {
+ setRestoring(true);
+ await restoreFromZip(file.fileCopyUri, true);
+ } else if (file.name.endsWith(".nnbackup")) {
+ RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
+ setRestoring(true);
+ const data = await fetch(file.uri);
+ await restoreFromNNBackup(await data.json());
+ }
+ } catch (e) {
+ console.log("error", e.stack);
+ setRestoring(false);
+ backupError(e);
+ }
+ }
+ };
+
const renderItem = ({ item, index }) => (
{
);
- const restoreBackup = async (backup, password) => {
- await db.backup.import(backup, password);
- setRestoring(false);
- initialize();
- ToastEvent.show({
- heading: "Backup restored successfully.",
- type: "success",
- context: "global"
- });
- };
-
- const backupError = (e) => {
- ToastEvent.show({
- heading: "Restore failed",
- message:
- e.message ||
- "The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
- type: "error",
- context: "local"
- });
- };
-
- const button = {
- title: "Restore from files",
- onPress: () => {
- if (restoring) {
- return;
- }
-
- DocumentPicker.pickSingle()
- .then((r) => {
- setRestoring(true);
- fetch(r.uri)
- .then(async (r) => {
- try {
- let backup = await r.json();
- if (backup.data.iv && backup.data.salt) {
- withPassword(
- async (value) => {
- try {
- restoreBackup(backup, value).then(() => {
- close();
- setRestoring(false);
- });
- return true;
- } catch (e) {
- backupError(e);
- setRestoring(false);
- return false;
- }
- },
- () => {
- setRestoring(false);
- }
- );
- } else {
- await restoreBackup(backup);
- close();
- }
- } catch (e) {
- setRestoring(false);
- backupError(e);
- }
- })
- .catch(console.log);
- })
- .catch(console.log);
- }
- };
-
return (
<>
diff --git a/apps/mobile/app/services/backup.js b/apps/mobile/app/services/backup.js
index fdeffce42..0cf58fdba 100644
--- a/apps/mobile/app/services/backup.js
+++ b/apps/mobile/app/services/backup.js
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { sanitizeFilename } from "@notesnook/common";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
@@ -30,6 +29,8 @@ import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastEvent, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
+import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
+import { zip } from "react-native-zip-archive";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -152,8 +153,6 @@ async function run(progress, context) {
let androidBackupDirectory = await checkBackupDirExists(false, context);
if (!androidBackupDirectory) return;
- let backup;
-
if (progress) {
presentSheet({
title: "Backing up your data",
@@ -163,43 +162,67 @@ async function run(progress, context) {
});
}
- try {
- backup = await db.backup.export(
- "mobile",
- SettingsService.get().encryptedBackup
- );
- if (!backup) throw new Error("Backup returned empty.");
- } catch (e) {
- await sleep(300);
- eSendEvent(eCloseSheet);
- ToastEvent.error(e, "Backup failed!");
- return null;
+ let path;
+ let backupFilePath;
+ let backupFileName = "notesnook_backup_" + Date.now();
+
+ if (Platform.OS === "ios") {
+ path = await storage.checkAndCreateDir("/backups/");
}
- try {
- let backupName = "notesnook_backup_" + Date.now();
- backupName =
- sanitizeFilename(backupName, { replacement: "_" }) + ".nnbackup";
- let path;
- let backupFilePath;
+ const zipSourceFolder = `${cacheDir}/${backupFileName}`;
+ const zipOutputFile =
+ Platform.OS === "ios"
+ ? `${path}/${backupFileName}.nnbackupz`
+ : `${cacheDir}/${backupFileName}.nnbackupz`;
- if (Platform.OS === "ios") {
- path = await storage.checkAndCreateDir("/backups/");
- await RNFetchBlob.fs.writeFile(path + backupName, backup, "utf8");
- backupFilePath = path + backupName;
- } else {
- backupFilePath = await ScopedStorage.writeFile(
- androidBackupDirectory.uri,
- backup,
- backupName,
- "nnbackup/json",
- "utf8",
- false
+ if (await RNFetchBlob.fs.exists(zipSourceFolder))
+ await RNFetchBlob.fs.unlink(zipSourceFolder);
+
+ await RNFetchBlob.fs.mkdir(zipSourceFolder);
+
+ try {
+ for await (const file of db.backup.export(
+ "mobile",
+ SettingsService.get().encryptedBackup
+ )) {
+ console.log("Writing backup chunk of size...", file?.data?.length);
+ await RNFetchBlob.fs.writeFile(
+ `${zipSourceFolder}/${file.path}`,
+ file.data,
+ "utf8"
);
}
+ await zip(zipSourceFolder, zipOutputFile);
+
+ console.log("Final zip:", await RNFetchBlob.fs.stat(zipOutputFile));
+
+ if (Platform.OS === "android") {
+ // Move the zip to user selected directory.
+ const file = await ScopedStorage.createFile(
+ androidBackupDirectory.uri,
+ `${backupFileName}.nnbackupz`,
+ "application/nnbackupz"
+ );
+ await copyFileAsync(`file://${zipOutputFile}`, file.uri);
+ path = file.uri;
+ } else {
+ path = zipOutputFile;
+ }
+ RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
updateNextBackupTime();
+ let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
+
+ if (context) return path;
+ await sleep(300);
+ if (showBackupCompleteSheet) {
+ presentBackupCompleteSheet(backupFilePath);
+ } else {
+ progress && eSendEvent(eCloseSheet);
+ }
+
ToastEvent.show({
heading: "Backup successful",
message: "Your backup is stored in Notesnook folder on your phone.",
@@ -207,18 +230,10 @@ async function run(progress, context) {
context: "global"
});
- let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
-
- if (context) return backupFilePath;
- await sleep(300);
- if (showBackupCompleteSheet) {
- presentBackupCompleteSheet(backupFilePath);
- } else {
- progress && eSendEvent(eCloseSheet);
- }
- return backupFilePath;
+ return path;
} catch (e) {
- progress && eSendEvent(eCloseSheet);
+ await sleep(300);
+ eSendEvent(eCloseSheet);
ToastEvent.error(e, "Backup failed!");
return null;
}