web: schedule automatic backups

This will increase backup reliability since before backups happened either
at app startup or after successful sync. This could create the issue where
backups would not be created regularly. This new scheduler based
implementation fixes this by scheduling automatic backups
according to user's settings.
This commit is contained in:
Abdullah Atta
2022-10-26 14:43:16 +05:00
parent 12202f74a7
commit cded505ec6
7 changed files with 257 additions and 40 deletions

View File

@@ -24,6 +24,8 @@
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"axios": "^0.21.4", "axios": "^0.21.4",
"clipboard-polyfill": "^3.0.3", "clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"event-source-polyfill": "^1.0.25", "event-source-polyfill": "^1.0.25",
"fflate": "^0.7.2", "fflate": "^0.7.2",
@@ -6779,6 +6781,11 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/comlink": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.3.1.tgz",
"integrity": "sha512-+YbhUdNrpBZggBAHWcgQMLPLH1KDF3wJpeqrCKieWQ8RL7atmgsgTQko1XEBK6PsecfopWNntopJ+ByYG1lRaA=="
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -7014,6 +7021,14 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/cronosjs": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/cronosjs/-/cronosjs-1.7.1.tgz",
"integrity": "sha512-d6S6+ep7dJxsAG8OQQCdKuByI/S/AV64d9OF5mtmcykOyPu92cAkAnF3Tbc9s5oOaLQBYYQmTNvjqYRkPJ/u5Q==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -24614,6 +24629,11 @@
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
}, },
"comlink": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.3.1.tgz",
"integrity": "sha512-+YbhUdNrpBZggBAHWcgQMLPLH1KDF3wJpeqrCKieWQ8RL7atmgsgTQko1XEBK6PsecfopWNntopJ+ByYG1lRaA=="
},
"commander": { "commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -24795,6 +24815,11 @@
} }
} }
}, },
"cronosjs": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/cronosjs/-/cronosjs-1.7.1.tgz",
"integrity": "sha512-d6S6+ep7dJxsAG8OQQCdKuByI/S/AV64d9OF5mtmcykOyPu92cAkAnF3Tbc9s5oOaLQBYYQmTNvjqYRkPJ/u5Q=="
},
"cross-spawn": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",

View File

@@ -30,6 +30,8 @@
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"axios": "^0.21.4", "axios": "^0.21.4",
"clipboard-polyfill": "^3.0.3", "clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"event-source-polyfill": "^1.0.25", "event-source-polyfill": "^1.0.25",
"fflate": "^0.7.2", "fflate": "^0.7.2",

View File

@@ -24,7 +24,7 @@ import { useStore as useNotesStore } from "./stores/note-store";
import { useStore as useThemeStore } from "./stores/theme-store"; import { useStore as useThemeStore } from "./stores/theme-store";
import { useStore as useAttachmentStore } from "./stores/attachment-store"; import { useStore as useAttachmentStore } from "./stores/attachment-store";
import { useStore as useEditorStore } from "./stores/editor-store"; import { useStore as useEditorStore } from "./stores/editor-store";
import { resetReminders } from "./common/reminders"; import { resetReminders, scheduleBackups } from "./common/reminders";
import { introduceFeatures, showUpgradeReminderDialogs } from "./common"; import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
import { AppEventManager, AppEvents } from "./common/app-events"; import { AppEventManager, AppEvents } from "./common/app-events";
import { db } from "./common/db"; import { db } from "./common/db";
@@ -103,6 +103,7 @@ export default function AppEffects({ setShow }) {
await showOnboardingDialog(interruptedOnboarding()); await showOnboardingDialog(interruptedOnboarding());
await showFeatureDialog("highlights"); await showFeatureDialog("highlights");
await scheduleBackups();
})(); })();
return () => { return () => {

View File

@@ -23,15 +23,54 @@ import { db } from "./db";
import { store as appStore } from "../stores/app-store"; import { store as appStore } from "../stores/app-store";
import * as Icon from "../components/icons"; import * as Icon from "../components/icons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { showRecoveryKeyDialog } from "../common/dialog-controller"; import {
showBuyDialog,
showRecoveryKeyDialog
} from "../common/dialog-controller";
import { hardNavigate, hashNavigate } from "../navigation"; import { hardNavigate, hashNavigate } from "../navigation";
import { isDesktop, isTesting } from "../utils/platform"; import { isDesktop, isTesting } from "../utils/platform";
import { isUserPremium } from "../hooks/use-is-user-premium"; import { isUserPremium } from "../hooks/use-is-user-premium";
import { showToast } from "../utils/toast"; import { showToast } from "../utils/toast";
import { TaskScheduler } from "../utils/task-scheduler";
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 async function scheduleBackups() {
const backupReminderOffset = Config.get(
"backupReminderOffset",
isUserPremium() ? 1 : 0
);
await TaskScheduler.stop("automatic-backups");
if (!backupReminderOffset) return false;
console.log("Scheduling automatic backups");
await TaskScheduler.register(
"automatic-backups",
BACKUP_CRON_EXPRESSIONS[backupReminderOffset],
() => {
console.log("Backing up automatically");
saveBackup();
}
);
}
export function shouldAddAutoBackupsDisabledReminder() {
const backupReminderOffset = Config.get("backupReminderOffset", 0);
if (!isUserPremium() && backupReminderOffset) {
Config.set("backupReminderOffset", 0);
return true;
}
return false;
}
export async function shouldAddBackupReminder() { export async function shouldAddBackupReminder() {
if (isIgnored("backup")) return false;
const backupReminderOffset = Config.get( const backupReminderOffset = Config.get(
"backupReminderOffset", "backupReminderOffset",
isUserPremium() ? 1 : 0 isUserPremium() ? 1 : 0
@@ -39,7 +78,10 @@ export async function shouldAddBackupReminder() {
if (!backupReminderOffset) return false; if (!backupReminderOffset) return false;
const lastBackupTime = await db.backup.lastBackupTime(); const lastBackupTime = await db.backup.lastBackupTime();
if (!lastBackupTime) return true; if (!lastBackupTime) {
await db.backup.updateBackupTime();
return false;
}
const offsetToDays = const offsetToDays =
backupReminderOffset === 1 ? 1 : backupReminderOffset === 2 ? 7 : 30; backupReminderOffset === 1 ? 1 : backupReminderOffset === 2 ? 7 : 30;
@@ -66,14 +108,12 @@ export async function shouldAddConfirmEmailReminder() {
} }
export const Reminders = { export const Reminders = {
backup: { autoBackupsOff: {
key: "backup", key: "autoBackupsOff",
subtitle: "Create a backup to keep your notes safe", title: "Automatic backups disabled",
subtitle: "Please upgrade to Pro to enable automatic backups.",
action: () => showBuyDialog(),
dismissable: true, dismissable: true,
title: "Back up your data",
action: async () => {
if (await verifyAccount()) await createBackup();
},
icon: Icon.Backup icon: Icon.Backup
}, },
login: { login: {
@@ -106,7 +146,29 @@ var openedToast = null;
export async function resetReminders() { export async function resetReminders() {
const reminders = []; const reminders = [];
if (shouldAddAutoBackupsDisabledReminder()) {
reminders.push({ type: "autoBackupsOff", priority: "high" });
}
if (await shouldAddBackupReminder()) { if (await shouldAddBackupReminder()) {
await saveBackup();
}
if (await shouldAddLoginReminder()) {
reminders.push({ type: "login", priority: "low" });
}
if (await shouldAddConfirmEmailReminder()) {
reminders.push({ type: "email", priority: "high" });
}
if (await shouldAddRecoveryKeyBackupReminder()) {
reminders.push({ type: "recoverykey", priority: "high" });
}
appStore.get().setReminders(...reminders);
}
function isIgnored(key) {
return Config.get(`ignored:${key}`, false);
}
async function saveBackup() {
if (isDesktop()) { if (isDesktop()) {
await createBackup(); await createBackup();
} else if (isUserPremium() && !isTesting()) { } else if (isUserPremium() && !isTesting()) {
@@ -133,19 +195,4 @@ export async function resetReminders() {
0 0
); );
} }
}
if (await shouldAddLoginReminder()) {
reminders.push({ type: "login", priority: "low" });
}
if (await shouldAddConfirmEmailReminder()) {
reminders.push({ type: "email", priority: "high" });
}
if (await shouldAddRecoveryKeyBackupReminder()) {
reminders.push({ type: "recoverykey", priority: "high" });
}
appStore.get().setReminders(...reminders);
}
function isIgnored(key) {
return Config.get(`ignored:${key}`, false);
} }

View File

@@ -0,0 +1,68 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2022 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Worker from "worker-loader?filename=static/workers/task-scheduler.worker.[hash].js!./task-scheduler.worker";
import type {
TaskScheduler as TaskSchedulerType,
TaskSchedulerEvent
} from "./task-scheduler.worker";
import { wrap, Remote } from "comlink";
let worker: globalThis.Worker | undefined;
let scheduler: Remote<TaskSchedulerType> | undefined;
export class TaskScheduler {
static async register(id: string, time: string, action: () => void) {
init();
worker?.addEventListener("message", function handler(ev) {
const { data } = ev as MessageEvent<TaskSchedulerEvent>;
if (data.id !== id) return;
switch (data.type) {
case "task-run":
action();
break;
case "task-stop":
case "task-end":
worker?.removeEventListener("message", handler);
break;
}
});
await scheduler?.registerTask(id, time);
}
static async stop(id: string) {
await scheduler?.stop(id);
}
static async stopAll() {
await scheduler?.stopAll();
worker?.terminate();
}
}
function init() {
if (worker) return;
worker = new Worker();
if (worker) scheduler = wrap<TaskSchedulerType>(worker);
}

View File

@@ -0,0 +1,67 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2022 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { CronosTask, CronosExpression } from "cronosjs";
import { expose } from "comlink";
const RUNNING_TASKS: Record<string, CronosTask> = {};
const module = {
registerTask: (id: string, pattern: string) => {
if (RUNNING_TASKS[id]) module.stop(id);
const expression = CronosExpression.parse(pattern, { strict: true });
const task = new CronosTask(expression);
task
.on("started", () => {
console.log("started", id, pattern);
RUNNING_TASKS[id] = task;
})
.on("run", () => {
globalThis.postMessage({ type: "task-run", id });
})
.on("stopped", () => {
console.log("stopping", id, pattern);
globalThis.postMessage({ type: "task-stop", id });
})
.on("ended", () => {
globalThis.postMessage({ type: "task-end", id });
delete RUNNING_TASKS[id];
})
.start();
},
stop: (id: string) => {
if (RUNNING_TASKS[id] && RUNNING_TASKS[id].isRunning) {
RUNNING_TASKS[id].stop();
delete RUNNING_TASKS[id];
}
},
stopAll: () => {
for (const id in RUNNING_TASKS) {
module.stop(id);
}
}
};
expose(module);
export type TaskScheduler = typeof module;
export type TaskSchedulerEvent = {
type: "task-run" | "task-stop" | "task-end";
id: string;
};

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components"; import { Button, Flex, Text } from "@theme-ui/components";
import * as Icon from "../components/icons"; import * as Icon from "../components/icons";
import { useStore as useUserStore } from "../stores/user-store"; import { useStore as useUserStore } from "../stores/user-store";
@@ -65,6 +65,7 @@ import { getAllAccents } from "@notesnook/theme";
import { debounce } from "../utils/debounce"; import { debounce } from "../utils/debounce";
import { clearLogs, downloadLogs } from "../utils/logger"; import { clearLogs, downloadLogs } from "../utils/logger";
import { exportNotes } from "../common/export"; import { exportNotes } from "../common/export";
import { scheduleBackups } from "../common/reminders";
function subscriptionStatusToString(user) { function subscriptionStatusToString(user) {
const status = user?.subscription?.type; const status = user?.subscription?.type;
@@ -174,6 +175,12 @@ function Settings() {
true true
); );
useEffect(() => {
(async () => {
await scheduleBackups();
})();
}, [backupReminderOffset]);
return ( return (
<FlexScrollContainer style={{ height: "100%" }}> <FlexScrollContainer style={{ height: "100%" }}>
<Flex variant="columnFill" px={2}> <Flex variant="columnFill" px={2}>
@@ -563,7 +570,7 @@ function Settings() {
options={["Never", "Daily", "Weekly", "Monthly"]} options={["Never", "Daily", "Weekly", "Monthly"]}
premium="backups" premium="backups"
selectedOption={backupReminderOffset} selectedOption={backupReminderOffset}
onSelectionChanged={(_option, index) => onSelectionChanged={async (_option, index) =>
setBackupReminderOffset(index) setBackupReminderOffset(index)
} }
/> />