mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
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:
25
apps/web/package-lock.json
generated
25
apps/web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
68
apps/web/src/utils/task-scheduler.ts
Normal file
68
apps/web/src/utils/task-scheduler.ts
Normal 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);
|
||||
}
|
||||
67
apps/web/src/utils/task-scheduler.worker.ts
Normal file
67
apps/web/src/utils/task-scheduler.worker.ts
Normal 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;
|
||||
};
|
||||
@@ -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)
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user