web: add options to create backups with attachments

This commit is contained in:
Abdullah Atta
2024-07-26 14:50:47 +05:00
committed by Abdullah Atta
parent 10e24ae632
commit 759fba06ae
7 changed files with 118 additions and 32 deletions

View File

@@ -22,7 +22,11 @@ import { useStore } from "./stores/app-store";
import { useStore as useUserStore } from "./stores/user-store";
import { useEditorStore } from "./stores/editor-store";
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
import { resetNotices, scheduleBackups } from "./common/notices";
import {
resetNotices,
scheduleBackups,
scheduleFullBackups
} from "./common/notices";
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
import { AppEventManager, AppEvents } from "./common/app-events";
import { db } from "./common/db";
@@ -90,6 +94,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
if (onboardingKey) await OnboardingDialog.show({ type: onboardingKey });
await FeatureDialog.show({ featureName: "highlights" });
await scheduleBackups();
await scheduleFullBackups();
})();
return () => {

View File

@@ -82,9 +82,10 @@ export async function createBackup(
options: {
rescueMode?: boolean;
noVerify?: boolean;
} = {}
mode?: "full" | "partial";
} = { mode: "partial" }
) {
const { rescueMode, noVerify } = options;
const { rescueMode, noVerify, mode } = options;
const { isLoggedIn } = useUserStore.getState();
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
@@ -103,7 +104,7 @@ export async function createBackup(
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
})}-${new Date().getSeconds()}`,
})}-${new Date().getSeconds()}${mode === "full" ? "-full" : ""}`,
{ replacement: "-" }
);
const directory = Config.get("backupStorageLocation", PATHS.backupsDirectory);
@@ -123,10 +124,11 @@ export async function createBackup(
start() {},
async pull(controller) {
const { streamablefs } = await import("../interfaces/fs");
for await (const output of db.backup!.export(
"web",
encryptedBackups
)) {
for await (const output of db.backup!.export({
type: "web",
encrypt: encryptedBackups,
mode
})) {
if (output.type === "file") {
const file = output;
report({

View File

@@ -38,12 +38,18 @@ export type Notice = {
params?: any;
};
export const BACKUP_CRON_EXPRESSIONS = [
"",
"0 0 8 * * *", // daily at 8 AM
"0 0 8 * * 1", // Every monday at 8 AM
"0 0 0 1 * *" // 1st day of every month
];
export const BACKUP_CRON_EXPRESSIONS = {
0: "",
1: "0 0 8 * * *", // daily at 8 AM
2: "0 0 8 * * 1", // Every monday at 8 AM
3: "0 0 0 1 * *" // 1st day of every month
};
export const FULL_BACKUP_CRON_EXPRESSIONS = {
0: "",
1: "0 0 8 * * 1", // Every monday at 8 AM
2: "0 0 0 1 * *" // 1st day of every month
};
export async function scheduleBackups() {
const backupInterval = Config.get("backupReminderOffset", 0);
@@ -62,6 +68,23 @@ export async function scheduleBackups() {
);
}
export async function scheduleFullBackups() {
const backupInterval = Config.get("fullBackupReminderOffset", 0);
await TaskScheduler.stop("automatic-full-backups");
if (!backupInterval) return false;
console.log("Scheduling automatic full backups");
await TaskScheduler.register(
"automatic-full-backups",
FULL_BACKUP_CRON_EXPRESSIONS[backupInterval],
() => {
console.log("Backing up automatically");
saveBackup("full");
}
);
}
export function shouldAddAutoBackupsDisabledNotice() {
const backupInterval = Config.get("backupReminderOffset", 0);
if (!isUserPremium() && backupInterval) {
@@ -175,9 +198,9 @@ function isIgnored(key: keyof typeof NoticesData) {
}
let openedToast: { hide: () => void } | null = null;
async function saveBackup() {
async function saveBackup(mode: "full" | "partial" = "partial") {
if (IS_DESKTOP_APP) {
await createBackup({ noVerify: true });
await createBackup({ noVerify: true, mode });
} else if (isUserPremium() && !IS_TESTING) {
if (openedToast !== null) return;
openedToast = showToast(
@@ -196,7 +219,7 @@ async function saveBackup() {
{
text: "Download",
onClick: async () => {
await createBackup();
await createBackup({ mode });
openedToast?.hide();
openedToast = null;
},

View File

@@ -37,14 +37,24 @@ export const BackupExportSettings: SettingsGroup[] = [
settings: [
{
key: "create-backup",
title: "Backup now",
description: "Create a backup file containing all your data",
title: "Create backup",
description:
"Partial backups contain all your data except attachments. They are created from data already available on your device and do not require an Internet connection.",
components: [
{
type: "button",
title: "Create backup",
action: createBackup,
variant: "secondary"
type: "dropdown",
options: [
{ value: "-", title: "Choose backup format" },
{ value: "partial", title: "Backup" },
{ value: "full", title: "Backup with attachments" }
],
selectedOption: () => "-",
onSelectionChanged: async (value) => {
if (value === "-") return;
await createBackup({
mode: value === "partial" ? "partial" : "full"
});
}
}
]
},
@@ -67,10 +77,10 @@ export const BackupExportSettings: SettingsGroup[] = [
},
{
key: "auto-backup",
title: IS_DESKTOP_APP ? "Automatic backups" : "Backup reminders",
description: IS_DESKTOP_APP
? "Backup all your data automatically at a set interval."
: "You will be shown regular reminders to backup your data.",
title: "Automatic backups",
description: `Set the interval to create a backup automatically.
Note: these backups do not contain attachments.`,
isHidden: () => !isUserPremium(),
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.backupReminderOffset, listener),
@@ -97,6 +107,40 @@ export const BackupExportSettings: SettingsGroup[] = [
}
]
},
{
key: "auto-backup-with-attachments",
title: "Automatic backup with attachments",
description: `Set the interval to create a backup (with attachments) automatically.
NOTE: Creating a backup with attachments can take a while, and also fail completely. The app will try to resume/restart the backup in case of interruptions.`,
isHidden: () => !isUserPremium(),
onStateChange: (listener) =>
useSettingStore.subscribe(
(s) => s.fullBackupReminderOffset,
listener
),
components: [
{
type: "dropdown",
options: [
{ value: "0", title: "Never" },
{ value: "1", title: "Weekly" },
{ value: "2", title: "Monthly" }
],
selectedOption: () =>
useSettingStore.getState().fullBackupReminderOffset.toString(),
onSelectionChanged: async (value) => {
const verified =
useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (verified)
useSettingStore
.getState()
.setFullBackupReminderOffset(parseInt(value));
}
}
]
},
{
key: "encrypt-backups",
title: "Backup encryption",

View File

@@ -32,6 +32,7 @@ import { Profile, TrashCleanupInterval } from "@notesnook/core";
class SettingStore extends BaseStore<SettingStore> {
encryptBackups = Config.get("encryptBackups", false);
backupReminderOffset = Config.get("backupReminderOffset", 0);
fullBackupReminderOffset = Config.get("fullBackupReminderOffset", 0);
backupStorageLocation = Config.get(
"backupStorageLocation",
PATHS.backupsDirectory
@@ -149,6 +150,11 @@ class SettingStore extends BaseStore<SettingStore> {
this.set({ backupReminderOffset: offset });
};
setFullBackupReminderOffset = (offset: number) => {
Config.set("fullBackupReminderOffset", offset);
this.set({ fullBackupReminderOffset: offset });
};
setBackupStorageLocation = (location: string) => {
Config.set("backupStorageLocation", location);
this.set({ backupStorageLocation: location });

View File

@@ -455,7 +455,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
"Please wait while we create a backup file for you to download."
}}
onSubmit={async () => {
await createBackup({ rescueMode: true });
await createBackup({ rescueMode: true, mode: "full" });
navigate("new");
}}
>

View File

@@ -277,10 +277,11 @@ export default class Backup {
await this.updateBackupTime();
}
async *export(
type: BackupPlatform,
encrypt = false
): AsyncGenerator<
async *export(options: {
type: BackupPlatform;
encrypt?: boolean;
mode?: "full" | "partial";
}): AsyncGenerator<
| {
type: "file";
path: string;
@@ -296,6 +297,7 @@ export default class Backup {
void,
unknown
> {
const { encrypt = false, type, mode = "partial" } = options;
if (this.db.migrations.version === 5.9) {
yield* this.exportLegacy(type, encrypt);
return;
@@ -343,6 +345,10 @@ export default class Backup {
yield* this.backupCollection(this.db.vaults.collection, backupState);
if (backupState.buffer.length > 0) yield* this.bufferToFile(backupState);
if (mode === "partial") {
await this.updateBackupTime();
return;
}
const total = await this.db.attachments.all.count();
if (total > 0 && user && user.attachmentsKey) {