Skip to content

Commit a43cb3c

Browse files
committed
feat: add schedule local backup (#1359)
1 parent ecf02b9 commit a43cb3c

File tree

4 files changed

+197
-10
lines changed

4 files changed

+197
-10
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"i18n-ally.localesPaths": [
33
"src/locales"
4-
]
4+
],
5+
"i18n-ally.keystyle": "nested"
56
}

src/background/BackgroundEventsListeners.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,65 @@
11
import browser from 'webextension-polyfill';
22
import { initElementSelector } from '@/newtab/utils/elementSelector';
3+
import dayjs from 'dayjs';
4+
import cronParser from 'cron-parser';
35
import BackgroundUtils from './BackgroundUtils';
46
import BackgroundWorkflowTriggers from './BackgroundWorkflowTriggers';
57

8+
async function handleScheduleBackup() {
9+
try {
10+
const { scheduleLocalBackup, workflows } = await browser.storage.local.get([
11+
'scheduleLocalBackup',
12+
'workflows',
13+
]);
14+
if (!scheduleLocalBackup) return;
15+
16+
const workflowsData = Object.values(workflows || []).reduce(
17+
(acc, workflow) => {
18+
if (workflow.isProtected) return acc;
19+
20+
delete workflow.$id;
21+
delete workflow.createdAt;
22+
delete workflow.data;
23+
delete workflow.isDisabled;
24+
delete workflow.isProtected;
25+
26+
acc.push(workflow);
27+
28+
return acc;
29+
},
30+
[]
31+
);
32+
const base64 = btoa(JSON.stringify(workflowsData));
33+
const filename = `${
34+
scheduleLocalBackup.folderName ? `${scheduleLocalBackup.folderName}/` : ''
35+
}${dayjs().format('DD-MMM-YYYY--HH-mm')}.json`;
36+
37+
await browser.downloads.download({
38+
filename,
39+
url: `data:application/json;base64,${base64}`,
40+
});
41+
await browser.storage.local.set({
42+
scheduleLocalBackup: {
43+
...scheduleLocalBackup,
44+
lastBackup: Date.now(),
45+
},
46+
});
47+
48+
const expression =
49+
scheduleLocalBackup.schedule === 'custom'
50+
? scheduleLocalBackup.customSchedule
51+
: scheduleLocalBackup.schedule;
52+
const parsedExpression = cronParser.parseExpression(expression).next();
53+
if (!parsedExpression) return;
54+
55+
await browser.alarms.create('schedule-local-backup', {
56+
when: parsedExpression.getTime(),
57+
});
58+
} catch (error) {
59+
console.error(error);
60+
}
61+
}
62+
663
class BackgroundEventsListeners {
764
static onActionClicked() {
865
BackgroundUtils.openDashboard();
@@ -17,6 +74,11 @@ class BackgroundEventsListeners {
1774
}
1875

1976
static onAlarms(event) {
77+
if (event.name === 'schedule-local-backup') {
78+
handleScheduleBackup();
79+
return;
80+
}
81+
2082
BackgroundWorkflowTriggers.scheduleWorkflow(event);
2183
}
2284

src/locales/en/newtab.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@
153153
"needSignin": "You need to sign in first",
154154
"backup": {
155155
"button": "Backup",
156-
"encrypt": "Encrypt with password"
156+
"encrypt": "Encrypt with password",
157+
"schedule": "Schedule local backup"
157158
},
158159
"restore": {
159160
"title": "Restore workflows",

src/newtab/pages/settings/SettingsBackup.vue

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,80 @@
8080
<ui-checkbox v-model="state.encrypt" class="mt-12 mb-4">
8181
{{ t('settings.backupWorkflows.backup.encrypt') }}
8282
</ui-checkbox>
83-
<ui-button class="w-full" @click="backupWorkflows">
84-
{{ t('settings.backupWorkflows.backup.button') }}
85-
</ui-button>
83+
<div class="flex items-center gap-2">
84+
<ui-button class="flex-1" @click="backupWorkflows">
85+
{{ t('settings.backupWorkflows.backup.button') }}
86+
</ui-button>
87+
<ui-popover @close="registerScheduleBackup">
88+
<template #trigger>
89+
<ui-button
90+
v-tooltip="t('settings.backupWorkflows.backup.schedule')"
91+
icon
92+
:class="{ 'text-primary': localBackupSchedule.schedule }"
93+
>
94+
<v-remixicon name="riCalendarLine" />
95+
</ui-button>
96+
</template>
97+
<div class="min-w-[14rem]">
98+
<p class="mb-2">
99+
{{ t('settings.backupWorkflows.backup.schedule') }}
100+
</p>
101+
<template v-if="!downloadPermission.has.downloads">
102+
<p class="text-gray-600 dark:text-gray-300">
103+
Automa requires the "Downloads" permission for this feature to
104+
work
105+
</p>
106+
<ui-button
107+
class="mt-4 w-full"
108+
@click="downloadPermission.request()"
109+
>
110+
Allow "Downloads" permission
111+
</ui-button>
112+
</template>
113+
<template v-else>
114+
<ui-select
115+
v-model="localBackupSchedule.schedule"
116+
label="Schedule"
117+
class="w-full"
118+
>
119+
<option value="">Never</option>
120+
<option
121+
v-for="(value, key) in BACKUP_SCHEDULES"
122+
:key="key"
123+
:value="key"
124+
>
125+
{{ value }}
126+
</option>
127+
<option value="custom">Custom</option>
128+
</ui-select>
129+
<template v-if="localBackupSchedule.schedule === 'custom'">
130+
<ui-input
131+
v-model="localBackupSchedule.customSchedule"
132+
label="Cron Expression"
133+
class="w-full mt-2"
134+
placeholder="0 8 * * *"
135+
/>
136+
<p className="text-sm text-gray-600 dark:text-gray-300">
137+
{{ getBackupScheduleCron() }}
138+
</p>
139+
</template>
140+
<ui-input
141+
v-model="localBackupSchedule.folderName"
142+
label="Folder name"
143+
class="w-full mt-2"
144+
placeholder="backup-folder"
145+
/>
146+
<p
147+
v-if="localBackupSchedule.lastBackup"
148+
class="text-gray-600 dark:text-gray-300 text-sm mt-4"
149+
>
150+
Last backup:
151+
{{ dayjs(localBackupSchedule.lastBackup).fromNow() }}
152+
</p>
153+
</template>
154+
</div>
155+
</ui-popover>
156+
</div>
86157
</div>
87158
<div class="w-6/12 rounded-lg border p-4 dark:border-gray-700">
88159
<div class="text-center">
@@ -111,26 +182,35 @@
111182
</ui-modal>
112183
</template>
113184
<script setup>
114-
import { reactive, onMounted } from 'vue';
185+
import { reactive, toRaw, onMounted } from 'vue';
115186
import { useI18n } from 'vue-i18n';
116187
import { useToast } from 'vue-toastification';
117188
import dayjs from 'dayjs';
118189
import AES from 'crypto-js/aes';
190+
import cronParser from 'cron-parser';
119191
import encUtf8 from 'crypto-js/enc-utf8';
120192
import browser from 'webextension-polyfill';
121193
import hmacSHA256 from 'crypto-js/hmac-sha256';
122194
import { useDialog } from '@/composable/dialog';
195+
import { readableCron } from '@/lib/cronstrue';
123196
import { useUserStore } from '@/stores/user';
124197
import { getUserWorkflows } from '@/utils/api';
125198
import { useWorkflowStore } from '@/stores/workflow';
199+
import { useHasPermissions } from '@/composable/hasPermissions';
126200
import { fileSaver, openFilePicker, parseJSON } from '@/utils/helper';
127201
import SettingsCloudBackup from '@/components/newtab/settings/SettingsCloudBackup.vue';
128202
203+
const BACKUP_SCHEDULES = {
204+
'0 8 * * *': 'Every day',
205+
'0 8 * * 0': 'Every week',
206+
};
207+
129208
const { t } = useI18n();
130209
const toast = useToast();
131210
const dialog = useDialog();
132211
const userStore = useUserStore();
133212
const workflowStore = useWorkflowStore();
213+
const downloadPermission = useHasPermissions(['downloads']);
134214
135215
const state = reactive({
136216
lastSync: null,
@@ -144,7 +224,46 @@ const backupState = reactive({
144224
modal: false,
145225
loading: false,
146226
});
227+
const localBackupSchedule = reactive({
228+
schedule: '',
229+
lastBackup: null,
230+
customSchedule: '',
231+
folderName: 'automa-backup',
232+
});
233+
234+
async function registerScheduleBackup() {
235+
try {
236+
if (!localBackupSchedule.schedule.trim()) {
237+
await browser.alarms.clear('schedule-local-backup');
238+
} else {
239+
const expression =
240+
localBackupSchedule.schedule === 'custom'
241+
? localBackupSchedule.customSchedule
242+
: localBackupSchedule.schedule;
243+
const parsedExpression = cronParser.parseExpression(expression).next();
244+
if (!parsedExpression) return;
245+
246+
await browser.alarms.create('schedule-local-backup', {
247+
when: parsedExpression.getTime(),
248+
});
249+
}
147250
251+
browser.storage.local.set({
252+
scheduleLocalBackup: toRaw(localBackupSchedule),
253+
});
254+
} catch (error) {
255+
console.error(error);
256+
}
257+
}
258+
function getBackupScheduleCron() {
259+
try {
260+
const expression = localBackupSchedule.customSchedule;
261+
262+
return `${readableCron(expression)}`;
263+
} catch (error) {
264+
return error.message;
265+
}
266+
}
148267
function formatDate(date) {
149268
if (!date) return 'null';
150269
@@ -301,10 +420,14 @@ async function restoreWorkflows() {
301420
}
302421
303422
onMounted(async () => {
304-
const { lastBackup, lastSync } = await browser.storage.local.get([
305-
'lastBackup',
306-
'lastSync',
307-
]);
423+
const { lastBackup, lastSync, scheduleLocalBackup } =
424+
await browser.storage.local.get([
425+
'lastSync',
426+
'lastBackup',
427+
'scheduleLocalBackup',
428+
]);
429+
430+
Object.assign(localBackupSchedule, scheduleLocalBackup || {});
308431
309432
state.lastSync = lastSync;
310433
state.lastBackup = lastBackup;

0 commit comments

Comments
 (0)