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",
"axios": "^0.21.4",
"clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "^1.10.4",
"event-source-polyfill": "^1.0.25",
"fflate": "^0.7.2",
@@ -6779,6 +6781,11 @@
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -7014,6 +7021,14 @@
"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": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -24614,6 +24629,11 @@
"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": {
"version": "4.1.1",
"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": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",

View File

@@ -30,6 +30,8 @@
"async-mutex": "^0.3.2",
"axios": "^0.21.4",
"clipboard-polyfill": "^3.0.3",
"comlink": "^4.3.1",
"cronosjs": "^1.7.1",
"dayjs": "^1.10.4",
"event-source-polyfill": "^1.0.25",
"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 useAttachmentStore } from "./stores/attachment-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 { AppEventManager, AppEvents } from "./common/app-events";
import { db } from "./common/db";
@@ -103,6 +103,7 @@ export default function AppEffects({ setShow }) {
await showOnboardingDialog(interruptedOnboarding());
await showFeatureDialog("highlights");
await scheduleBackups();
})();
return () => {

View File

@@ -23,15 +23,54 @@ import { db } from "./db";
import { store as appStore } from "../stores/app-store";
import * as Icon from "../components/icons";
import dayjs from "dayjs";
import { showRecoveryKeyDialog } from "../common/dialog-controller";
import {
showBuyDialog,
showRecoveryKeyDialog
} from "../common/dialog-controller";
import { hardNavigate, hashNavigate } from "../navigation";
import { isDesktop, isTesting } from "../utils/platform";
import { isUserPremium } from "../hooks/use-is-user-premium";
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() {
if (isIgnored("backup")) return false;
const backupReminderOffset = Config.get(
"backupReminderOffset",
isUserPremium() ? 1 : 0
@@ -39,7 +78,10 @@ export async function shouldAddBackupReminder() {
if (!backupReminderOffset) return false;
const lastBackupTime = await db.backup.lastBackupTime();
if (!lastBackupTime) return true;
if (!lastBackupTime) {
await db.backup.updateBackupTime();
return false;
}
const offsetToDays =
backupReminderOffset === 1 ? 1 : backupReminderOffset === 2 ? 7 : 30;
@@ -66,14 +108,12 @@ export async function shouldAddConfirmEmailReminder() {
}
export const Reminders = {
backup: {
key: "backup",
subtitle: "Create a backup to keep your notes safe",
autoBackupsOff: {
key: "autoBackupsOff",
title: "Automatic backups disabled",
subtitle: "Please upgrade to Pro to enable automatic backups.",
action: () => showBuyDialog(),
dismissable: true,
title: "Back up your data",
action: async () => {
if (await verifyAccount()) await createBackup();
},
icon: Icon.Backup
},
login: {
@@ -106,7 +146,29 @@ var openedToast = null;
export async function resetReminders() {
const reminders = [];
if (shouldAddAutoBackupsDisabledReminder()) {
reminders.push({ type: "autoBackupsOff", priority: "high" });
}
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()) {
await createBackup();
} else if (isUserPremium() && !isTesting()) {
@@ -133,19 +195,4 @@ export async function resetReminders() {
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/>.
*/
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import * as Icon from "../components/icons";
import { useStore as useUserStore } from "../stores/user-store";
@@ -65,6 +65,7 @@ import { getAllAccents } from "@notesnook/theme";
import { debounce } from "../utils/debounce";
import { clearLogs, downloadLogs } from "../utils/logger";
import { exportNotes } from "../common/export";
import { scheduleBackups } from "../common/reminders";
function subscriptionStatusToString(user) {
const status = user?.subscription?.type;
@@ -174,6 +175,12 @@ function Settings() {
true
);
useEffect(() => {
(async () => {
await scheduleBackups();
})();
}, [backupReminderOffset]);
return (
<FlexScrollContainer style={{ height: "100%" }}>
<Flex variant="columnFill" px={2}>
@@ -563,7 +570,7 @@ function Settings() {
options={["Never", "Daily", "Weekly", "Monthly"]}
premium="backups"
selectedOption={backupReminderOffset}
onSelectionChanged={(_option, index) =>
onSelectionChanged={async (_option, index) =>
setBackupReminderOffset(index)
}
/>