Skip to content

Commit

Permalink
feat: export tables & variables (AutomaApp#659)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kholid060 committed Sep 12, 2023
1 parent a43cb3c commit 43f2503
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 80 deletions.
35 changes: 25 additions & 10 deletions src/background/BackgroundEventsListeners.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import browser from 'webextension-polyfill';
import { initElementSelector } from '@/newtab/utils/elementSelector';
import dayjs from 'dayjs';
import dbStorage from '@/db/storage';
import cronParser from 'cron-parser';
import BackgroundUtils from './BackgroundUtils';
import BackgroundWorkflowTriggers from './BackgroundWorkflowTriggers';

async function handleScheduleBackup() {
try {
const { scheduleLocalBackup, workflows } = await browser.storage.local.get([
'scheduleLocalBackup',
const { localBackupSettings, workflows } = await browser.storage.local.get([
'localBackupSettings',
'workflows',
]);
if (!scheduleLocalBackup) return;
if (!localBackupSettings) return;

const workflowsData = Object.values(workflows || []).reduce(
(acc, workflow) => {
Expand All @@ -29,26 +30,40 @@ async function handleScheduleBackup() {
},
[]
);
const base64 = btoa(JSON.stringify(workflowsData));

const payload = {
workflows: JSON.stringify(workflowsData),
};

if (localBackupSettings.includedItems.includes('storage:table')) {
const tables = await dbStorage.tablesItems.toArray();
payload.storageTables = JSON.stringify(tables);
}
if (localBackupSettings.includedItems.includes('storage:variables')) {
const variables = await dbStorage.variables.toArray();
payload.storageVariables = JSON.stringify(variables);
}

const base64 = btoa(JSON.stringify(payload));
const filename = `${
scheduleLocalBackup.folderName ? `${scheduleLocalBackup.folderName}/` : ''
localBackupSettings.folderName ? `${localBackupSettings.folderName}/` : ''
}${dayjs().format('DD-MMM-YYYY--HH-mm')}.json`;

await browser.downloads.download({
filename,
url: `data:application/json;base64,${base64}`,
});
await browser.storage.local.set({
scheduleLocalBackup: {
...scheduleLocalBackup,
localBackupSettings: {
...localBackupSettings,
lastBackup: Date.now(),
},
});

const expression =
scheduleLocalBackup.schedule === 'custom'
? scheduleLocalBackup.customSchedule
: scheduleLocalBackup.schedule;
localBackupSettings.schedule === 'custom'
? localBackupSettings.customSchedule
: localBackupSettings.schedule;
const parsedExpression = cronParser.parseExpression(expression).next();
if (!parsedExpression) return;

Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/UiSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{{ label }}
</slot>
</label>
<div class="ui-select__content relative block flex w-full items-center">
<div class="ui-select__content relative flex w-full items-center">
<v-remixicon
v-if="prependIcon"
size="20"
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/newtab.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"needSignin": "You need to sign in first",
"backup": {
"button": "Backup",
"settings": "Backup settings",
"encrypt": "Encrypt with password",
"schedule": "Schedule local backup"
},
Expand Down
190 changes: 121 additions & 69 deletions src/newtab/pages/settings/SettingsBackup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,30 +81,50 @@
{{ t('settings.backupWorkflows.backup.encrypt') }}
</ui-checkbox>
<div class="flex items-center gap-2">
<ui-button class="flex-1" @click="backupWorkflows">
{{ t('settings.backupWorkflows.backup.button') }}
</ui-button>
<ui-popover @close="registerScheduleBackup">
<template #trigger>
<ui-button
v-tooltip="t('settings.backupWorkflows.backup.schedule')"
v-tooltip="t('settings.backupWorkflows.backup.settings')"
icon
:class="{ 'text-primary': localBackupSchedule.schedule }"
>
<v-remixicon name="riCalendarLine" />
<v-remixicon name="riSettings3Line" />
</ui-button>
</template>
<div class="min-w-[14rem]">
<p class="mb-2">
<div class="w-64">
<p class="mb-2 font-semibold">
{{ t('settings.backupWorkflows.backup.settings') }}
</p>
<p>Also backup</p>
<div class="flex mt-1 flex-col gap-2">
<ui-checkbox
v-for="item in BACKUP_ITEMS_INCLUDES"
:key="item.id"
:model-value="
localBackupSchedule.includedItems.includes(item.id)
"
@change="
$event
? localBackupSchedule.includedItems.push(item.id)
: localBackupSchedule.includedItems.splice(
localBackupSchedule.includedItems.indexOf(item.id),
1
)
"
>
{{ item.name }}
</ui-checkbox>
</div>
<p class="mt-4">
{{ t('settings.backupWorkflows.backup.schedule') }}
</p>
<template v-if="!downloadPermission.has.downloads">
<p class="text-gray-600 dark:text-gray-300">
Automa requires the "Downloads" permission for this feature to
work
<p class="text-gray-600 dark:text-gray-300 mt-1">
Automa requires the "Downloads" permission for the schedule
backup to work
</p>
<ui-button
class="mt-4 w-full"
class="mt-2 w-full"
@click="downloadPermission.request()"
>
Allow "Downloads" permission
Expand All @@ -113,8 +133,7 @@
<template v-else>
<ui-select
v-model="localBackupSchedule.schedule"
label="Schedule"
class="w-full"
class="w-full mt-2"
>
<option value="">Never</option>
<option
Expand All @@ -138,6 +157,7 @@
</p>
</template>
<ui-input
v-if="localBackupSchedule.schedule !== ''"
v-model="localBackupSchedule.folderName"
label="Folder name"
class="w-full mt-2"
Expand All @@ -153,6 +173,9 @@
</template>
</div>
</ui-popover>
<ui-button class="flex-1" @click="backupWorkflows">
{{ t('settings.backupWorkflows.backup.button') }}
</ui-button>
</div>
</div>
<div class="w-6/12 rounded-lg border p-4 dark:border-gray-700">
Expand Down Expand Up @@ -188,6 +211,7 @@ import { useToast } from 'vue-toastification';
import dayjs from 'dayjs';
import AES from 'crypto-js/aes';
import cronParser from 'cron-parser';
import dbStorage from '@/db/storage';
import encUtf8 from 'crypto-js/enc-utf8';
import browser from 'webextension-polyfill';
import hmacSHA256 from 'crypto-js/hmac-sha256';
Expand All @@ -204,6 +228,10 @@ const BACKUP_SCHEDULES = {
'0 8 * * *': 'Every day',
'0 8 * * 0': 'Every week',
};
const BACKUP_ITEMS_INCLUDES = [
{ id: 'storage:table', name: 'Storage tables' },
{ id: 'storage:variables', name: 'Storage variables' },
];
const { t } = useI18n();
const toast = useToast();
Expand All @@ -227,6 +255,7 @@ const backupState = reactive({
const localBackupSchedule = reactive({
schedule: '',
lastBackup: null,
includedItems: [],
customSchedule: '',
folderName: 'automa-backup',
});
Expand All @@ -249,7 +278,7 @@ async function registerScheduleBackup() {
}
browser.storage.local.set({
scheduleLocalBackup: toRaw(localBackupSchedule),
localBackupSettings: toRaw(localBackupSchedule),
});
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -292,56 +321,70 @@ async function syncBackupWorkflows() {
state.loadingSync = false;
}
}
function backupWorkflows() {
const workflows = workflowStore.getWorkflows.reduce((acc, workflow) => {
if (workflow.isProtected) return acc;
delete workflow.$id;
delete workflow.createdAt;
delete workflow.data;
delete workflow.isDisabled;
delete workflow.isProtected;
acc.push(workflow);
return acc;
}, []);
const payload = {
isProtected: state.encrypt,
workflows: JSON.stringify(workflows),
};
const downloadFile = (data) => {
const fileName = `automa-${dayjs().format('DD-MM-YYYY')}.json`;
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const objectUrl = URL.createObjectURL(blob);
fileSaver(fileName, objectUrl);
URL.revokeObjectURL(objectUrl);
};
if (state.encrypt) {
dialog.prompt({
placeholder: t('common.password'),
title: t('settings.backupWorkflows.title'),
okText: t('settings.backupWorkflows.backup.button'),
inputType: 'password',
onConfirm: (password) => {
const encryptedWorkflows = AES.encrypt(
payload.workflows,
password
).toString();
const hmac = hmacSHA256(encryptedWorkflows, password).toString();
payload.workflows = hmac + encryptedWorkflows;
downloadFile(payload);
},
});
} else {
downloadFile(payload);
async function backupWorkflows() {
try {
const workflows = workflowStore.getWorkflows.reduce((acc, workflow) => {
if (workflow.isProtected) return acc;
delete workflow.$id;
delete workflow.createdAt;
delete workflow.data;
delete workflow.isDisabled;
delete workflow.isProtected;
acc.push(workflow);
return acc;
}, []);
const payload = {
isProtected: state.encrypt,
workflows: JSON.stringify(workflows),
};
if (localBackupSchedule.includedItems.includes('storage:table')) {
const tables = await dbStorage.tablesItems.toArray();
payload.storageTables = JSON.stringify(tables);
}
if (localBackupSchedule.includedItems.includes('storage:variables')) {
const variables = await dbStorage.variables.toArray();
payload.storageVariables = JSON.stringify(variables);
}
const downloadFile = (data) => {
const fileName = `automa-${dayjs().format('DD-MM-YYYY')}.json`;
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const objectUrl = URL.createObjectURL(blob);
fileSaver(fileName, objectUrl);
URL.revokeObjectURL(objectUrl);
};
if (state.encrypt) {
dialog.prompt({
placeholder: t('common.password'),
title: t('settings.backupWorkflows.title'),
okText: t('settings.backupWorkflows.backup.button'),
inputType: 'password',
onConfirm: (password) => {
const encryptedWorkflows = AES.encrypt(
payload.workflows,
password
).toString();
const hmac = hmacSHA256(encryptedWorkflows, password).toString();
payload.workflows = hmac + encryptedWorkflows;
downloadFile(payload);
},
});
} else {
downloadFile(payload);
}
} catch (error) {
console.error(error);
}
}
async function restoreWorkflows() {
Expand All @@ -360,7 +403,7 @@ async function restoreWorkflows() {
const showMessage = (event) => {
toast(
t('settings.backupWorkflows.workflowsAdded', {
count: event.workflows.length,
count: Object.values(event).length,
})
);
};
Expand All @@ -374,9 +417,18 @@ async function restoreWorkflows() {
reader.onload = ({ target }) => {
const payload = parseJSON(target.result, null);
if (!payload) return;
const storageTables = parseJSON(payload.storageTables, null);
if (Array.isArray(storageTables)) {
dbStorage.tablesItems.bulkPut(storageTables);
}
const storageVariables = parseJSON(payload.storageVariables, null);
if (Array.isArray(storageVariables)) {
dbStorage.variables.bulkPut(storageVariables);
}
if (payload.isProtected) {
dialog.prompt({
placeholder: t('common.password'),
Expand Down Expand Up @@ -420,14 +472,14 @@ async function restoreWorkflows() {
}
onMounted(async () => {
const { lastBackup, lastSync, scheduleLocalBackup } =
const { lastBackup, lastSync, localBackupSettings } =
await browser.storage.local.get([
'lastSync',
'lastBackup',
'scheduleLocalBackup',
'localBackupSettings',
]);
Object.assign(localBackupSchedule, scheduleLocalBackup || {});
Object.assign(localBackupSchedule, localBackupSettings || {});
state.lastSync = lastSync;
state.lastBackup = lastBackup;
Expand Down

0 comments on commit 43f2503

Please sign in to comment.