@actual-app/core 26.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- package/vitest.web.config.ts +38 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import * as dateFns from 'date-fns';
|
|
3
|
+
|
|
4
|
+
import { updateBackups } from './backups';
|
|
5
|
+
|
|
6
|
+
describe('Backups', () => {
|
|
7
|
+
test('backups work', async () => {
|
|
8
|
+
async function getUpdatedBackups(backups) {
|
|
9
|
+
const toRemove = await updateBackups(backups);
|
|
10
|
+
return backups.filter(b => !toRemove.includes(b.id));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanDates(backups) {
|
|
14
|
+
return backups.map(backup => ({
|
|
15
|
+
id: backup.id,
|
|
16
|
+
date: dateFns.format(backup.date, 'yyyy-MM-dd'),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Should keep 3 backups on the current day
|
|
21
|
+
expect(
|
|
22
|
+
cleanDates(
|
|
23
|
+
await getUpdatedBackups([
|
|
24
|
+
{ id: 'backup1', date: dateFns.parseISO('2017-01-01') },
|
|
25
|
+
{ id: 'backup2', date: dateFns.parseISO('2017-01-01') },
|
|
26
|
+
{ id: 'backup3', date: dateFns.parseISO('2017-01-01') },
|
|
27
|
+
{ id: 'backup4', date: dateFns.parseISO('2017-01-01') },
|
|
28
|
+
]),
|
|
29
|
+
),
|
|
30
|
+
).toMatchSnapshot();
|
|
31
|
+
|
|
32
|
+
// Should not delete any since up to 3 are allowed on the current
|
|
33
|
+
// day
|
|
34
|
+
expect(
|
|
35
|
+
cleanDates(
|
|
36
|
+
await getUpdatedBackups([
|
|
37
|
+
{ id: 'backup1', date: dateFns.parseISO('2017-01-01') },
|
|
38
|
+
{ id: 'backup2', date: dateFns.parseISO('2017-01-01') },
|
|
39
|
+
{ id: 'backup3', date: dateFns.parseISO('2016-12-30') },
|
|
40
|
+
{ id: 'backup4', date: dateFns.parseISO('2016-12-29') },
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
).toMatchSnapshot();
|
|
44
|
+
|
|
45
|
+
// Should delete any additional backups on other days (keep the
|
|
46
|
+
// two on the current day but delete copies on other days)
|
|
47
|
+
expect(
|
|
48
|
+
cleanDates(
|
|
49
|
+
await getUpdatedBackups([
|
|
50
|
+
{ id: 'backup1', date: dateFns.parseISO('2017-01-01') },
|
|
51
|
+
{ id: 'backup2', date: dateFns.parseISO('2017-01-01') },
|
|
52
|
+
{ id: 'backup3', date: dateFns.parseISO('2016-12-29') },
|
|
53
|
+
{ id: 'backup4', date: dateFns.parseISO('2016-12-29') },
|
|
54
|
+
{ id: 'backup5', date: dateFns.parseISO('2016-12-29') },
|
|
55
|
+
]),
|
|
56
|
+
),
|
|
57
|
+
).toMatchSnapshot();
|
|
58
|
+
|
|
59
|
+
// Should only keep up to 10 backups
|
|
60
|
+
expect(
|
|
61
|
+
cleanDates(
|
|
62
|
+
await getUpdatedBackups([
|
|
63
|
+
{ id: 'backup1', date: dateFns.parseISO('2017-01-01') },
|
|
64
|
+
{ id: 'backup2', date: dateFns.parseISO('2017-01-01') },
|
|
65
|
+
{ id: 'backup3', date: dateFns.parseISO('2016-12-29') },
|
|
66
|
+
{ id: 'backup4', date: dateFns.parseISO('2016-12-28') },
|
|
67
|
+
{ id: 'backup5', date: dateFns.parseISO('2016-12-27') },
|
|
68
|
+
{ id: 'backup6', date: dateFns.parseISO('2016-12-26') },
|
|
69
|
+
{ id: 'backup7', date: dateFns.parseISO('2016-12-25') },
|
|
70
|
+
{ id: 'backup8', date: dateFns.parseISO('2016-12-24') },
|
|
71
|
+
{ id: 'backup9', date: dateFns.parseISO('2016-12-23') },
|
|
72
|
+
{ id: 'backup10', date: dateFns.parseISO('2016-12-22') },
|
|
73
|
+
{ id: 'backup11', date: dateFns.parseISO('2016-12-21') },
|
|
74
|
+
{ id: 'backup12', date: dateFns.parseISO('2016-12-20') },
|
|
75
|
+
]),
|
|
76
|
+
),
|
|
77
|
+
).toMatchSnapshot();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Database } from '@jlongster/sql.js';
|
|
2
|
+
// @ts-strict-ignore
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
|
+
import * as dateFns from 'date-fns';
|
|
5
|
+
|
|
6
|
+
import * as connection from '../../platform/server/connection';
|
|
7
|
+
import * as fs from '../../platform/server/fs';
|
|
8
|
+
import { logger } from '../../platform/server/log';
|
|
9
|
+
import * as sqlite from '../../platform/server/sqlite';
|
|
10
|
+
import * as monthUtils from '../../shared/months';
|
|
11
|
+
import * as cloudStorage from '../cloud-storage';
|
|
12
|
+
import * as prefs from '../prefs';
|
|
13
|
+
|
|
14
|
+
// A special backup that represents the latest version of the db that
|
|
15
|
+
// can be reverted to after loading a backup
|
|
16
|
+
const LATEST_BACKUP_FILENAME = 'db.latest.sqlite';
|
|
17
|
+
let serviceInterval = null;
|
|
18
|
+
|
|
19
|
+
export type Backup = { id: string; date: string } | LatestBackup;
|
|
20
|
+
type LatestBackup = { id: string; date: null; isLatest: true };
|
|
21
|
+
type BackupWithDate = { id: string; date: Date };
|
|
22
|
+
|
|
23
|
+
async function getBackups(id: string): Promise<BackupWithDate[]> {
|
|
24
|
+
const budgetDir = fs.getBudgetDir(id);
|
|
25
|
+
const backupDir = fs.join(budgetDir, 'backups');
|
|
26
|
+
|
|
27
|
+
let paths = [];
|
|
28
|
+
if (await fs.exists(backupDir)) {
|
|
29
|
+
paths = await fs.listDir(backupDir);
|
|
30
|
+
paths = paths.filter(file => file.match(/\.zip$/));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const backups = await Promise.all(
|
|
34
|
+
paths.map(async path => {
|
|
35
|
+
const mtime = await fs.getModifiedTime(fs.join(backupDir, path));
|
|
36
|
+
return {
|
|
37
|
+
id: path,
|
|
38
|
+
date: new Date(mtime),
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
backups.sort((b1, b2) => {
|
|
44
|
+
if (b1.date < b2.date) {
|
|
45
|
+
return 1;
|
|
46
|
+
} else if (b1.date > b2.date) {
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return backups;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getLatestBackup(id: string): Promise<LatestBackup | null> {
|
|
56
|
+
const budgetDir = fs.getBudgetDir(id);
|
|
57
|
+
if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
|
|
58
|
+
return {
|
|
59
|
+
id: LATEST_BACKUP_FILENAME,
|
|
60
|
+
date: null,
|
|
61
|
+
isLatest: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getAvailableBackups(id: string): Promise<Backup[]> {
|
|
68
|
+
const backups = await getBackups(id);
|
|
69
|
+
|
|
70
|
+
const latestBackup = await getLatestBackup(id);
|
|
71
|
+
if (latestBackup) {
|
|
72
|
+
backups.unshift(latestBackup);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return backups.map(backup => ({
|
|
76
|
+
...backup,
|
|
77
|
+
date: backup.date ? dateFns.format(backup.date, 'yyyy-MM-dd H:mm') : null,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function updateBackups(backups) {
|
|
82
|
+
const byDay = backups.reduce((groups, backup) => {
|
|
83
|
+
const day = dateFns.format(backup.date, 'yyyy-MM-dd');
|
|
84
|
+
groups[day] = groups[day] || [];
|
|
85
|
+
groups[day].push(backup);
|
|
86
|
+
return groups;
|
|
87
|
+
}, {});
|
|
88
|
+
|
|
89
|
+
const removed = [];
|
|
90
|
+
for (const day of Object.keys(byDay)) {
|
|
91
|
+
const dayBackups = byDay[day];
|
|
92
|
+
const isToday = day === monthUtils.currentDay();
|
|
93
|
+
// Allow 3 backups of the current day (so fine-grained edits are
|
|
94
|
+
// kept around). Otherwise only keep around one backup per day.
|
|
95
|
+
// And only keep a total of 10 backups.
|
|
96
|
+
for (const backup of dayBackups.slice(isToday ? 3 : 1)) {
|
|
97
|
+
removed.push(backup.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get the list of remaining backups and only keep the latest 10
|
|
102
|
+
const currentBackups = backups.filter(backup => !removed.includes(backup.id));
|
|
103
|
+
return removed.concat(currentBackups.slice(10).map(backup => backup.id));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function makeBackup(id: string) {
|
|
107
|
+
const budgetDir = fs.getBudgetDir(id);
|
|
108
|
+
|
|
109
|
+
// When making a backup, we no longer consider the user to be
|
|
110
|
+
// viewing any backups. If there exists a "latest backup" we should
|
|
111
|
+
// delete it and consider whatever is current as the latest
|
|
112
|
+
if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
|
|
113
|
+
await fs.removeFile(fs.join(fs.getBudgetDir(id), LATEST_BACKUP_FILENAME));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const backupId = `${dateFns.format(new Date(), 'yyyy-MM-dd_HH-mm-ss')}.zip`;
|
|
117
|
+
const backupPath = fs.join(budgetDir, 'backups', backupId);
|
|
118
|
+
|
|
119
|
+
if (!(await fs.exists(fs.join(budgetDir, 'backups')))) {
|
|
120
|
+
await fs.mkdir(fs.join(budgetDir, 'backups'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Copy db to a temp path so we can clean CRDT messages before zipping
|
|
124
|
+
const tempDbPath = fs.join(
|
|
125
|
+
budgetDir,
|
|
126
|
+
'backups',
|
|
127
|
+
`db.${Date.now()}.sqlite.tmp`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await fs.copyFile(fs.join(budgetDir, 'db.sqlite'), tempDbPath);
|
|
131
|
+
|
|
132
|
+
let db: Database | undefined;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Remove all the messages from the backup
|
|
136
|
+
db = await sqlite.openDatabase(tempDbPath);
|
|
137
|
+
sqlite.runQuery(db, 'DELETE FROM messages_crdt');
|
|
138
|
+
sqlite.runQuery(db, 'DELETE FROM messages_clock');
|
|
139
|
+
// Zip up the cleaned db and metadata into a single backup file
|
|
140
|
+
const zip = new AdmZip();
|
|
141
|
+
zip.addLocalFile(tempDbPath, '', 'db.sqlite');
|
|
142
|
+
zip.addLocalFile(fs.join(budgetDir, 'metadata.json'));
|
|
143
|
+
zip.writeZip(backupPath);
|
|
144
|
+
} finally {
|
|
145
|
+
if (db) {
|
|
146
|
+
sqlite.closeDatabase(db);
|
|
147
|
+
}
|
|
148
|
+
if (await fs.exists(tempDbPath)) {
|
|
149
|
+
await fs.removeFile(tempDbPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const toRemove = await updateBackups(await getBackups(id));
|
|
154
|
+
for (const id of toRemove) {
|
|
155
|
+
await fs.removeFile(fs.join(budgetDir, 'backups', id));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
connection.send('backups-updated', await getAvailableBackups(id));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function loadBackup(id: string, backupId: string) {
|
|
162
|
+
const budgetDir = fs.getBudgetDir(id);
|
|
163
|
+
|
|
164
|
+
if (!(await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME)))) {
|
|
165
|
+
// If this is the first time we're loading a backup, save the
|
|
166
|
+
// current version so the user can easily revert back to it
|
|
167
|
+
await fs.copyFile(
|
|
168
|
+
fs.join(budgetDir, 'db.sqlite'),
|
|
169
|
+
fs.join(budgetDir, LATEST_BACKUP_FILENAME),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await fs.copyFile(
|
|
173
|
+
fs.join(budgetDir, 'metadata.json'),
|
|
174
|
+
fs.join(budgetDir, 'metadata.latest.json'),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Restart the backup service to make sure the user has the full
|
|
178
|
+
// amount of time to figure out which one they want
|
|
179
|
+
stopBackupService();
|
|
180
|
+
startBackupService(id);
|
|
181
|
+
|
|
182
|
+
await prefs.loadPrefs(id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (backupId === LATEST_BACKUP_FILENAME) {
|
|
186
|
+
logger.log('Reverting backup');
|
|
187
|
+
|
|
188
|
+
// If reverting back to the latest, copy and delete the latest
|
|
189
|
+
// backup
|
|
190
|
+
await fs.copyFile(
|
|
191
|
+
fs.join(budgetDir, LATEST_BACKUP_FILENAME),
|
|
192
|
+
fs.join(budgetDir, 'db.sqlite'),
|
|
193
|
+
);
|
|
194
|
+
await fs.copyFile(
|
|
195
|
+
fs.join(budgetDir, 'metadata.latest.json'),
|
|
196
|
+
fs.join(budgetDir, 'metadata.json'),
|
|
197
|
+
);
|
|
198
|
+
await fs.removeFile(fs.join(budgetDir, LATEST_BACKUP_FILENAME));
|
|
199
|
+
await fs.removeFile(fs.join(budgetDir, 'metadata.latest.json'));
|
|
200
|
+
|
|
201
|
+
// Re-upload the new file
|
|
202
|
+
try {
|
|
203
|
+
await cloudStorage.upload();
|
|
204
|
+
} catch {}
|
|
205
|
+
prefs.unloadPrefs();
|
|
206
|
+
} else {
|
|
207
|
+
logger.log('Loading backup', backupId);
|
|
208
|
+
|
|
209
|
+
// This function is only ever called when a budget isn't loaded,
|
|
210
|
+
// so it's safe to load our prefs in. We need to forget about any
|
|
211
|
+
// syncing data if we are loading a backup (the current sync data
|
|
212
|
+
// will be restored if the user reverts to the original version)
|
|
213
|
+
await prefs.loadPrefs(id);
|
|
214
|
+
await prefs.savePrefs({
|
|
215
|
+
groupId: null,
|
|
216
|
+
lastSyncedTimestamp: null,
|
|
217
|
+
lastUploaded: null,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Re-upload the new file
|
|
221
|
+
try {
|
|
222
|
+
await cloudStorage.upload();
|
|
223
|
+
} catch {}
|
|
224
|
+
|
|
225
|
+
prefs.unloadPrefs();
|
|
226
|
+
|
|
227
|
+
const zip = new AdmZip(fs.join(budgetDir, 'backups', backupId));
|
|
228
|
+
zip.extractEntryTo('db.sqlite', budgetDir, false, true);
|
|
229
|
+
zip.extractEntryTo('metadata.json', budgetDir, false, true);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function startBackupService(id: string) {
|
|
234
|
+
if (serviceInterval) {
|
|
235
|
+
clearInterval(serviceInterval);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Make a backup every 15 minutes
|
|
239
|
+
serviceInterval = setInterval(
|
|
240
|
+
async () => {
|
|
241
|
+
logger.log('Making backup');
|
|
242
|
+
await makeBackup(id);
|
|
243
|
+
},
|
|
244
|
+
1000 * 60 * 15,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function stopBackupService() {
|
|
249
|
+
clearInterval(serviceInterval);
|
|
250
|
+
serviceInterval = null;
|
|
251
|
+
}
|