@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,467 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import AdmZip from 'adm-zip';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
import * as asyncStorage from '../platform/server/asyncStorage';
|
|
6
|
+
import { fetch } from '../platform/server/fetch';
|
|
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
|
+
|
|
12
|
+
import * as encryption from './encryption';
|
|
13
|
+
import {
|
|
14
|
+
FileDownloadError,
|
|
15
|
+
FileUploadError,
|
|
16
|
+
HTTPError,
|
|
17
|
+
PostError,
|
|
18
|
+
} from './errors';
|
|
19
|
+
import { runMutator } from './mutators';
|
|
20
|
+
import { post } from './post';
|
|
21
|
+
import * as prefs from './prefs';
|
|
22
|
+
import { getServer } from './server-config';
|
|
23
|
+
|
|
24
|
+
const UPLOAD_FREQUENCY_IN_DAYS = 7;
|
|
25
|
+
|
|
26
|
+
export type UsersWithAccess = {
|
|
27
|
+
userId: string;
|
|
28
|
+
userName: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
owner: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type RemoteFile = {
|
|
33
|
+
deleted: boolean;
|
|
34
|
+
fileId: string;
|
|
35
|
+
groupId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
encryptKeyId: string;
|
|
38
|
+
hasKey: boolean;
|
|
39
|
+
owner: string;
|
|
40
|
+
usersWithAccess: UsersWithAccess[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function checkHTTPStatus(res) {
|
|
44
|
+
if (res.status !== 200) {
|
|
45
|
+
if (res.status === 403) {
|
|
46
|
+
try {
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
const data = JSON.parse(text)?.data;
|
|
49
|
+
if (data?.reason === 'token-expired') {
|
|
50
|
+
await asyncStorage.removeItem('user-token');
|
|
51
|
+
throw new HTTPError(403, 'token-expired');
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e instanceof HTTPError) throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return res.text().then(str => {
|
|
58
|
+
throw new HTTPError(res.status, str);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function fetchJSON(...args: Parameters<typeof fetch>) {
|
|
66
|
+
let res = await fetch(...args);
|
|
67
|
+
res = await checkHTTPStatus(res);
|
|
68
|
+
return res.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function checkKey(): Promise<{
|
|
72
|
+
valid: boolean;
|
|
73
|
+
error?: { reason: string };
|
|
74
|
+
}> {
|
|
75
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
76
|
+
|
|
77
|
+
const { cloudFileId, encryptKeyId } = prefs.getPrefs();
|
|
78
|
+
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await post(getServer().SYNC_SERVER + '/user-get-key', {
|
|
82
|
+
token: userToken,
|
|
83
|
+
fileId: cloudFileId,
|
|
84
|
+
});
|
|
85
|
+
} catch (e) {
|
|
86
|
+
logger.log(e);
|
|
87
|
+
return { valid: false, error: { reason: 'network' } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
valid:
|
|
92
|
+
// This == comparison is important, they could be null or undefined
|
|
93
|
+
// oxlint-disable-next-line eslint/eqeqeq
|
|
94
|
+
res.id == encryptKeyId &&
|
|
95
|
+
(encryptKeyId == null || encryption.hasKey(encryptKeyId)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function resetSyncState(newKeyState) {
|
|
100
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
101
|
+
|
|
102
|
+
const { cloudFileId } = prefs.getPrefs();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await post(getServer().SYNC_SERVER + '/reset-user-file', {
|
|
106
|
+
token: userToken,
|
|
107
|
+
fileId: cloudFileId,
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e instanceof PostError) {
|
|
111
|
+
return {
|
|
112
|
+
error: {
|
|
113
|
+
reason: e.reason === 'unauthorized' ? 'unauthorized' : 'network',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { error: { reason: 'internal' } };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (newKeyState) {
|
|
121
|
+
try {
|
|
122
|
+
await post(getServer().SYNC_SERVER + '/user-create-key', {
|
|
123
|
+
token: userToken,
|
|
124
|
+
fileId: cloudFileId,
|
|
125
|
+
keyId: newKeyState.key.getId(),
|
|
126
|
+
keySalt: newKeyState.salt,
|
|
127
|
+
testContent: newKeyState.testContent,
|
|
128
|
+
});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e instanceof PostError) {
|
|
131
|
+
return { error: { reason: 'network' } };
|
|
132
|
+
}
|
|
133
|
+
return { error: { reason: 'internal' } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function exportBuffer() {
|
|
141
|
+
const { id, budgetName } = prefs.getPrefs();
|
|
142
|
+
if (!budgetName) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const budgetDir = fs.getBudgetDir(id);
|
|
147
|
+
|
|
148
|
+
// create zip
|
|
149
|
+
const zipped = new AdmZip();
|
|
150
|
+
|
|
151
|
+
// We run this in a mutator even though its not mutating anything
|
|
152
|
+
// because we are reading the sqlite file from disk. We want to make
|
|
153
|
+
// sure that we get a valid snapshot of it so we want this to be
|
|
154
|
+
// serialized with all other mutations.
|
|
155
|
+
await runMutator(async () => {
|
|
156
|
+
const rawDbContent = await fs.readFile(
|
|
157
|
+
fs.join(budgetDir, 'db.sqlite'),
|
|
158
|
+
'binary',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Do some post-processing of the database. We NEVER upload the cache with
|
|
162
|
+
// the database; this forces new downloads to always recompute everything
|
|
163
|
+
// which is not only safer, but reduces the filesize a lot.
|
|
164
|
+
const memDb = await sqlite.openDatabase(rawDbContent);
|
|
165
|
+
sqlite.execQuery(
|
|
166
|
+
memDb,
|
|
167
|
+
`
|
|
168
|
+
DELETE FROM kvcache;
|
|
169
|
+
DELETE FROM kvcache_key;
|
|
170
|
+
`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const dbContent = await sqlite.exportDatabase(memDb);
|
|
174
|
+
|
|
175
|
+
sqlite.closeDatabase(memDb);
|
|
176
|
+
|
|
177
|
+
// mark it as a file that needs a new clock so when a new client
|
|
178
|
+
// downloads it, it'll get set to a unique node
|
|
179
|
+
const meta = JSON.parse(
|
|
180
|
+
await fs.readFile(fs.join(budgetDir, 'metadata.json')),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
meta.resetClock = true;
|
|
184
|
+
const metaContent = Buffer.from(JSON.stringify(meta), 'utf8');
|
|
185
|
+
|
|
186
|
+
zipped.addFile('db.sqlite', Buffer.from(dbContent));
|
|
187
|
+
zipped.addFile('metadata.json', metaContent);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return Buffer.from(zipped.toBuffer());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function importBuffer(fileData, buffer) {
|
|
194
|
+
let zipped, entries;
|
|
195
|
+
try {
|
|
196
|
+
zipped = new AdmZip(buffer);
|
|
197
|
+
entries = zipped.getEntries();
|
|
198
|
+
} catch {
|
|
199
|
+
throw FileDownloadError('not-zip-file');
|
|
200
|
+
}
|
|
201
|
+
const dbEntry = entries.find(e => e.entryName.includes('db.sqlite'));
|
|
202
|
+
const metaEntry = entries.find(e => e.entryName.includes('metadata.json'));
|
|
203
|
+
|
|
204
|
+
if (!dbEntry || !metaEntry) {
|
|
205
|
+
throw FileDownloadError('invalid-zip-file');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const dbContent = zipped.readFile(dbEntry);
|
|
209
|
+
const metaContent = zipped.readFile(metaEntry);
|
|
210
|
+
|
|
211
|
+
let meta;
|
|
212
|
+
try {
|
|
213
|
+
meta = JSON.parse(metaContent.toString('utf8'));
|
|
214
|
+
} catch {
|
|
215
|
+
throw FileDownloadError('invalid-meta-file');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update the metadata. The stored file on the server might be
|
|
219
|
+
// out-of-date with a few keys
|
|
220
|
+
meta = {
|
|
221
|
+
...meta,
|
|
222
|
+
cloudFileId: fileData.fileId,
|
|
223
|
+
groupId: fileData.groupId,
|
|
224
|
+
lastUploaded: monthUtils.currentDay(),
|
|
225
|
+
encryptKeyId: fileData.encryptMeta ? fileData.encryptMeta.keyId : null,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const budgetDir = fs.getBudgetDir(meta.id);
|
|
229
|
+
|
|
230
|
+
if (await fs.exists(budgetDir)) {
|
|
231
|
+
// Don't remove the directory so that backups are retained
|
|
232
|
+
const dbFile = fs.join(budgetDir, 'db.sqlite');
|
|
233
|
+
const metaFile = fs.join(budgetDir, 'metadata.json');
|
|
234
|
+
|
|
235
|
+
if (await fs.exists(dbFile)) {
|
|
236
|
+
await fs.removeFile(dbFile);
|
|
237
|
+
}
|
|
238
|
+
if (await fs.exists(metaFile)) {
|
|
239
|
+
await fs.removeFile(metaFile);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
await fs.mkdir(budgetDir);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await fs.writeFile(fs.join(budgetDir, 'db.sqlite'), dbContent);
|
|
246
|
+
await fs.writeFile(fs.join(budgetDir, 'metadata.json'), JSON.stringify(meta));
|
|
247
|
+
|
|
248
|
+
return { id: meta.id };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function upload() {
|
|
252
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
253
|
+
if (!userToken) {
|
|
254
|
+
throw FileUploadError('unauthorized');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const zipContent = await exportBuffer();
|
|
258
|
+
if (zipContent == null) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const {
|
|
263
|
+
id,
|
|
264
|
+
groupId,
|
|
265
|
+
budgetName,
|
|
266
|
+
cloudFileId: originalCloudFileId,
|
|
267
|
+
encryptKeyId,
|
|
268
|
+
} = prefs.getPrefs();
|
|
269
|
+
let cloudFileId = originalCloudFileId;
|
|
270
|
+
let uploadContent = zipContent;
|
|
271
|
+
let uploadMeta = null;
|
|
272
|
+
|
|
273
|
+
// The upload process encrypts with the key tagged in the prefs for
|
|
274
|
+
// the file. It will upload the file and the server is responsible
|
|
275
|
+
// for checking that the key is up-to-date and rejecting it if not
|
|
276
|
+
if (encryptKeyId) {
|
|
277
|
+
let encrypted;
|
|
278
|
+
try {
|
|
279
|
+
encrypted = await encryption.encrypt(zipContent, encryptKeyId);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
throw FileUploadError('encrypt-failure', {
|
|
282
|
+
isMissingKey: e.message === 'missing-key',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
uploadContent = encrypted.value;
|
|
286
|
+
uploadMeta = encrypted.meta;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!cloudFileId) {
|
|
290
|
+
cloudFileId = uuidv4();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let res;
|
|
294
|
+
try {
|
|
295
|
+
res = await fetchJSON(getServer().SYNC_SERVER + '/upload-user-file', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: {
|
|
298
|
+
'Content-Length': String(uploadContent.length),
|
|
299
|
+
'Content-Type': 'application/encrypted-file',
|
|
300
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
301
|
+
'X-ACTUAL-FILE-ID': cloudFileId,
|
|
302
|
+
'X-ACTUAL-NAME': encodeURIComponent(budgetName),
|
|
303
|
+
'X-ACTUAL-FORMAT': '2',
|
|
304
|
+
...(uploadMeta
|
|
305
|
+
? { 'X-ACTUAL-ENCRYPT-META': JSON.stringify(uploadMeta) }
|
|
306
|
+
: null),
|
|
307
|
+
...(groupId ? { 'X-ACTUAL-GROUP-ID': groupId } : null),
|
|
308
|
+
// TODO: fix me
|
|
309
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
310
|
+
},
|
|
311
|
+
body: uploadContent,
|
|
312
|
+
});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
logger.log('Upload failure', err);
|
|
315
|
+
|
|
316
|
+
if (err instanceof PostError) {
|
|
317
|
+
throw FileUploadError(
|
|
318
|
+
err.reason === 'unauthorized'
|
|
319
|
+
? 'unauthorized'
|
|
320
|
+
: err.reason || 'network',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw FileUploadError('internal');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (res.status === 'ok') {
|
|
328
|
+
// Only save it if we are still working on the same file
|
|
329
|
+
if (prefs.getPrefs() && prefs.getPrefs().id === id) {
|
|
330
|
+
await prefs.savePrefs({
|
|
331
|
+
lastUploaded: monthUtils.currentDay(),
|
|
332
|
+
cloudFileId,
|
|
333
|
+
groupId: res.groupId,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
throw FileUploadError('internal');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function possiblyUpload() {
|
|
342
|
+
const { cloudFileId, groupId, lastUploaded } = prefs.getPrefs();
|
|
343
|
+
|
|
344
|
+
const threshold =
|
|
345
|
+
lastUploaded && monthUtils.addDays(lastUploaded, UPLOAD_FREQUENCY_IN_DAYS);
|
|
346
|
+
const currentDay = monthUtils.currentDay();
|
|
347
|
+
|
|
348
|
+
// We only want to try to upload every UPLOAD_FREQUENCY_IN_DAYS days
|
|
349
|
+
if (lastUploaded && currentDay < threshold) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// We only want to upload existing cloud files that are part of a
|
|
354
|
+
// valid group
|
|
355
|
+
if (!cloudFileId || !groupId) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Don't block on uploading
|
|
360
|
+
upload().catch(() => {
|
|
361
|
+
// Ignore errors
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function removeFile(fileId) {
|
|
366
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
367
|
+
|
|
368
|
+
await post(getServer().SYNC_SERVER + '/delete-user-file', {
|
|
369
|
+
token: userToken,
|
|
370
|
+
fileId,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function listRemoteFiles(): Promise<RemoteFile[]> {
|
|
375
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
376
|
+
if (!userToken) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let res;
|
|
381
|
+
try {
|
|
382
|
+
res = await fetchJSON(getServer().SYNC_SERVER + '/list-user-files', {
|
|
383
|
+
headers: {
|
|
384
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
} catch (e) {
|
|
388
|
+
logger.log('Unexpected error fetching file list from server', e);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (res.status === 'error') {
|
|
393
|
+
logger.log('Error fetching file list from server', res);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return res.data
|
|
398
|
+
.map(file => ({
|
|
399
|
+
...file,
|
|
400
|
+
hasKey: encryption.hasKey(file.encryptKeyId),
|
|
401
|
+
}))
|
|
402
|
+
.filter(Boolean);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function download(cloudFileId) {
|
|
406
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
407
|
+
const syncServer = getServer().SYNC_SERVER;
|
|
408
|
+
|
|
409
|
+
const userFileFetch = fetch(`${syncServer}/download-user-file`, {
|
|
410
|
+
headers: {
|
|
411
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
412
|
+
'X-ACTUAL-FILE-ID': cloudFileId,
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
.then(checkHTTPStatus)
|
|
416
|
+
.then(res => {
|
|
417
|
+
if (res.arrayBuffer) {
|
|
418
|
+
return res.arrayBuffer().then(ab => Buffer.from(ab));
|
|
419
|
+
}
|
|
420
|
+
return res.buffer();
|
|
421
|
+
})
|
|
422
|
+
.catch(err => {
|
|
423
|
+
logger.log('Download failure', err);
|
|
424
|
+
throw FileDownloadError('download-failure');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const userFileInfoFetch = fetchJSON(`${syncServer}/get-user-file-info`, {
|
|
428
|
+
headers: {
|
|
429
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
430
|
+
'X-ACTUAL-FILE-ID': cloudFileId,
|
|
431
|
+
},
|
|
432
|
+
}).catch(err => {
|
|
433
|
+
logger.log('Error fetching file info', err);
|
|
434
|
+
throw FileDownloadError('internal', { fileId: cloudFileId });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const [userFileInfoRes, userFileRes] = await Promise.all([
|
|
438
|
+
userFileInfoFetch,
|
|
439
|
+
userFileFetch,
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
if (userFileInfoRes.status !== 'ok') {
|
|
443
|
+
logger.log(
|
|
444
|
+
'Could not download file from the server. Are you sure you have the right file ID?',
|
|
445
|
+
userFileInfoRes,
|
|
446
|
+
);
|
|
447
|
+
throw FileDownloadError('internal', { fileId: cloudFileId });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const fileData = userFileInfoRes.data;
|
|
451
|
+
let buffer = userFileRes;
|
|
452
|
+
|
|
453
|
+
// The download process checks if the server gave us decrypt
|
|
454
|
+
// information. It is assumed that this key has already been loaded
|
|
455
|
+
// in, which is done in a previous step
|
|
456
|
+
if (fileData.encryptMeta) {
|
|
457
|
+
try {
|
|
458
|
+
buffer = await encryption.decrypt(buffer, fileData.encryptMeta);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
throw FileDownloadError('decrypt-failure', {
|
|
461
|
+
isMissingKey: e.message === 'missing-key',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return importBuffer(fileData, buffer);
|
|
467
|
+
}
|