use localforage

This commit is contained in:
fyears 2021-11-14 20:24:33 +08:00
parent be2b168c6c
commit 16c3c11fd8
5 changed files with 158 additions and 212 deletions

View File

@ -41,7 +41,7 @@
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"codemirror": "^5.63.1", "codemirror": "^5.63.1",
"lovefield-ts": "^0.7.0", "localforage": "^1.10.0",
"mime-types": "^2.1.33", "mime-types": "^2.1.33",
"obsidian": "^0.12.0", "obsidian": "^0.12.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",

View File

@ -1,22 +1,14 @@
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian"; import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
import * as lf from "lovefield-ts/dist/es6/lf.js";
import type { SyncPlanType } from "./sync"; import type { SyncPlanType } from "./sync";
import { import { readAllSyncPlanRecordTexts } from "./localdb";
insertSyncPlanRecord, import type { InternalDBs } from "./localdb";
clearAllSyncPlanRecords,
readAllSyncPlanRecordTexts,
} from "./localdb";
import { mkdirpInVault } from "./misc"; import { mkdirpInVault } from "./misc";
const DEFAULT_DEBUG_FOLDER = "_debug_save_remote/"; const DEFAULT_DEBUG_FOLDER = "_debug_save_remote/";
const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_"; const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_";
export const exportSyncPlansToFiles = async ( export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
db: lf.DatabaseConnection,
vault: Vault
) => {
console.log("exporting"); console.log("exporting");
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault); await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
const records = await readAllSyncPlanRecordTexts(db); const records = await readAllSyncPlanRecordTexts(db);

View File

@ -1,12 +1,14 @@
import * as lf from "lovefield-ts/dist/es6/lf.js"; import localforage from "localforage";
import { TAbstractFile, TFile, TFolder } from "obsidian"; import { TAbstractFile, TFile, TFolder } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./misc"; import type { SUPPORTED_SERVICES_TYPE } from "./misc";
import type { SyncPlanType } from "./sync"; import type { SyncPlanType } from "./sync";
export type DatabaseConnection = lf.DatabaseConnection; export type LocalForage = typeof localforage;
export const DEFAULT_DB_VERSION_NUMBER: number = 20211114;
export const DEFAULT_DB_NAME = "saveremotedb"; export const DEFAULT_DB_NAME = "saveremotedb";
export const DEFAULT_TBL_VERSION = "schemaversion";
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory"; export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory";
export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory"; export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory";
export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory"; export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory";
@ -16,128 +18,105 @@ export interface FileFolderHistoryRecord {
ctime: number; ctime: number;
mtime: number; mtime: number;
size: number; size: number;
action_when: number; actionWhen: number;
action_type: "delete" | "rename"; actionType: "delete" | "rename";
key_type: "folder" | "file"; keyType: "folder" | "file";
rename_to: string; renameTo: string;
} }
export interface SyncMetaMappingRecord { interface SyncMetaMappingRecord {
local_key: string; localKey: string;
remote_key: string; remoteKey: string;
local_size: number; localSize: number;
remote_size: number; remoteSize: number;
local_mtime: number; localMtime: number;
remote_mtime: number; remoteMtime: number;
remote_extra_key: string; remoteExtraKey: string;
remote_type: SUPPORTED_SERVICES_TYPE; remoteType: SUPPORTED_SERVICES_TYPE;
key_type: "folder" | "file"; keyType: "folder" | "file";
} }
interface SyncPlanRecord { interface SyncPlanRecord {
ts: number; ts: number;
remote_type: string; remoteType: string;
sync_plan: string; syncPlan: string;
}
export interface InternalDBs {
versionTbl: LocalForage;
deleteHistoryTbl: LocalForage;
syncMappingTbl: LocalForage;
syncPlansTbl: LocalForage;
} }
export const prepareDBs = async () => { export const prepareDBs = async () => {
const schemaBuilder = lf.schema.create(DEFAULT_DB_NAME, 1); const db = {
schemaBuilder versionTbl: localforage.createInstance({
.createTable(DEFAULT_TBL_DELETE_HISTORY) name: DEFAULT_DB_NAME,
.addColumn("id", lf.Type.INTEGER) storeName: DEFAULT_TBL_VERSION,
.addColumn("key", lf.Type.STRING) }),
.addColumn("ctime", lf.Type.INTEGER) deleteHistoryTbl: localforage.createInstance({
.addColumn("mtime", lf.Type.INTEGER) name: DEFAULT_DB_NAME,
.addColumn("size", lf.Type.INTEGER) storeName: DEFAULT_TBL_DELETE_HISTORY,
.addColumn("action_when", lf.Type.INTEGER) }),
.addColumn("action_type", lf.Type.STRING) syncMappingTbl: localforage.createInstance({
.addColumn("key_type", lf.Type.STRING) name: DEFAULT_DB_NAME,
.addPrimaryKey(["id"], true) storeName: DEFAULT_TBL_SYNC_MAPPING,
.addIndex("idxKey", ["key"]); }),
syncPlansTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_SYNC_PLANS_HISTORY,
}),
} as InternalDBs;
schemaBuilder const originalVersion = (await db.versionTbl.getItem("version")) as number;
.createTable(DEFAULT_TBL_SYNC_MAPPING) if (originalVersion === null) {
.addColumn("id", lf.Type.INTEGER) await db.versionTbl.setItem("version", DEFAULT_DB_VERSION_NUMBER);
.addColumn("local_key", lf.Type.STRING) } else if (originalVersion === DEFAULT_DB_VERSION_NUMBER) {
.addColumn("remote_key", lf.Type.STRING) // do nothing
.addColumn("local_size", lf.Type.INTEGER) } else {
.addColumn("remote_size", lf.Type.INTEGER) await migrateDBs(db, originalVersion, DEFAULT_DB_VERSION_NUMBER);
.addColumn("local_mtime", lf.Type.INTEGER) }
.addColumn("remote_mtime", lf.Type.INTEGER)
.addColumn("key_type", lf.Type.STRING)
.addColumn("remote_extra_key", lf.Type.STRING)
.addColumn("remote_type", lf.Type.STRING)
.addNullable([
"remote_extra_key",
"remote_mtime",
"remote_size",
"local_mtime",
])
.addPrimaryKey(["id"], true)
.addIndex("idxkey", ["local_key", "remote_key"]);
schemaBuilder
.createTable(DEFAULT_SYNC_PLANS_HISTORY)
.addColumn("id", lf.Type.INTEGER)
.addColumn("ts", lf.Type.INTEGER)
.addColumn("remote_type", lf.Type.STRING)
.addColumn("sync_plan", lf.Type.STRING)
.addPrimaryKey(["id"], true)
.addIndex("tskey", ["ts"]);
const db = await schemaBuilder.connect({
storeType: lf.DataStoreType.INDEXED_DB,
});
console.log("db connected"); console.log("db connected");
return db; return db;
}; };
export const destroyDBs = async (db: lf.DatabaseConnection) => { export const destroyDBs = async () => {
db.close(); await localforage.dropInstance({
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME); name: DEFAULT_DB_NAME,
req.onsuccess = (event) => { });
console.log("db deleted"); console.log("db deleted");
}; };
req.onblocked = (event) => {
console.warn("trying to delete db but it was blocked"); const migrateDBs = async (db: InternalDBs, oldVer: number, newVer: number) => {
}; if (oldVer === newVer) {
req.onerror = (event) => { return;
console.error("tried to delete db but something bad!"); }
console.error(event); // not implemented
}; throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
}; };
export const loadDeleteRenameHistoryTable = async ( export const loadDeleteRenameHistoryTable = async (db: InternalDBs) => {
db: lf.DatabaseConnection const records = [] as FileFolderHistoryRecord[];
) => { await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY); records.push(value as FileFolderHistoryRecord);
const tbl = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY); });
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
const records = await db return records;
.select()
.from(schema)
.orderBy(schema.col("action_when"), lf.Order.ASC)
.exec();
return records as FileFolderHistoryRecord[];
}; };
export const clearDeleteRenameHistoryOfKey = async ( export const clearDeleteRenameHistoryOfKey = async (
db: lf.DatabaseConnection, db: InternalDBs,
key: string key: string
) => { ) => {
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY); await db.deleteHistoryTbl.removeItem(key);
const tbl = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
await db.delete().from(tbl).where(tbl.col("key").eq(key)).exec();
}; };
export const insertDeleteRecord = async ( export const insertDeleteRecord = async (
db: lf.DatabaseConnection, db: InternalDBs,
fileOrFolder: TAbstractFile fileOrFolder: TAbstractFile
) => { ) => {
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
const tbl = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
// console.log(fileOrFolder); // console.log(fileOrFolder);
let k: FileFolderHistoryRecord; let k: FileFolderHistoryRecord;
if (fileOrFolder instanceof TFile) { if (fileOrFolder instanceof TFile) {
@ -146,10 +125,10 @@ export const insertDeleteRecord = async (
ctime: fileOrFolder.stat.ctime, ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime, mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size, size: fileOrFolder.stat.size,
action_when: Date.now(), actionWhen: Date.now(),
action_type: "delete", actionType: "delete",
key_type: "file", keyType: "file",
rename_to: "", renameTo: "",
}; };
} else if (fileOrFolder instanceof TFolder) { } else if (fileOrFolder instanceof TFolder) {
// key should endswith "/" // key should endswith "/"
@ -161,23 +140,20 @@ export const insertDeleteRecord = async (
ctime: 0, ctime: 0,
mtime: 0, mtime: 0,
size: 0, size: 0,
action_when: Date.now(), actionWhen: Date.now(),
action_type: "delete", actionType: "delete",
key_type: "folder", keyType: "folder",
rename_to: "", renameTo: "",
}; };
} }
const row = tbl.createRow(k); await db.deleteHistoryTbl.setItem(k.key, k);
await db.insertOrReplace().into(tbl).values([row]).exec();
}; };
export const insertRenameRecord = async ( export const insertRenameRecord = async (
db: lf.DatabaseConnection, db: InternalDBs,
fileOrFolder: TAbstractFile, fileOrFolder: TAbstractFile,
oldPath: string oldPath: string
) => { ) => {
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
const tbl = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
// console.log(fileOrFolder); // console.log(fileOrFolder);
let k: FileFolderHistoryRecord; let k: FileFolderHistoryRecord;
if (fileOrFolder instanceof TFile) { if (fileOrFolder instanceof TFile) {
@ -186,10 +162,10 @@ export const insertRenameRecord = async (
ctime: fileOrFolder.stat.ctime, ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime, mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size, size: fileOrFolder.stat.size,
action_when: Date.now(), actionWhen: Date.now(),
action_type: "rename", actionType: "rename",
key_type: "file", keyType: "file",
rename_to: fileOrFolder.path, renameTo: fileOrFolder.path,
}; };
} else if (fileOrFolder instanceof TFolder) { } else if (fileOrFolder instanceof TFolder) {
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`; const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
@ -201,25 +177,17 @@ export const insertRenameRecord = async (
ctime: 0, ctime: 0,
mtime: 0, mtime: 0,
size: 0, size: 0,
action_when: Date.now(), actionWhen: Date.now(),
action_type: "rename", actionType: "rename",
key_type: "folder", keyType: "folder",
rename_to: renameTo, renameTo: renameTo,
}; };
} }
const row = tbl.createRow(k); await db.deleteHistoryTbl.setItem(k.key, k);
await db.insertOrReplace().into(tbl).values([row]).exec();
};
export const getAllDeleteRenameRecords = async (db: lf.DatabaseConnection) => {
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
const res1 = await db.select().from(schema).exec();
const res2 = res1 as FileFolderHistoryRecord[];
return res2;
}; };
export const upsertSyncMetaMappingDataS3 = async ( export const upsertSyncMetaMappingDataS3 = async (
db: lf.DatabaseConnection, db: InternalDBs,
localKey: string, localKey: string,
localMTime: number, localMTime: number,
localSize: number, localSize: number,
@ -228,89 +196,78 @@ export const upsertSyncMetaMappingDataS3 = async (
remoteSize: number, remoteSize: number,
remoteExtraKey: string /* ETag from s3 */ remoteExtraKey: string /* ETag from s3 */
) => { ) => {
const schema = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING);
const aggregratedInfo: SyncMetaMappingRecord = { const aggregratedInfo: SyncMetaMappingRecord = {
local_key: localKey, localKey: localKey,
local_mtime: localMTime, localMtime: localMTime,
local_size: localSize, localSize: localSize,
remote_key: remoteKey, remoteKey: remoteKey,
remote_mtime: remoteMTime, remoteMtime: remoteMTime,
remote_size: remoteSize, remoteSize: remoteSize,
remote_extra_key: remoteExtraKey, remoteExtraKey: remoteExtraKey,
remote_type: "s3", remoteType: "s3",
key_type: localKey.endsWith("/") ? "folder" : "file", keyType: localKey.endsWith("/") ? "folder" : "file",
}; };
const row = schema.createRow(aggregratedInfo); await db.syncMappingTbl.setItem(remoteKey, aggregratedInfo);
await db.insertOrReplace().into(schema).values([row]).exec();
}; };
export const getSyncMetaMappingByRemoteKeyS3 = async ( export const getSyncMetaMappingByRemoteKeyS3 = async (
db: lf.DatabaseConnection, db: InternalDBs,
remoteKey: string, remoteKey: string,
remoteMTime: number, remoteMTime: number,
remoteExtraKey: string remoteExtraKey: string
) => { ) => {
const schema = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING); const potentialItem = (await db.syncMappingTbl.getItem(
const tbl = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING); remoteKey
const res = (await db )) as SyncMetaMappingRecord;
.select()
.from(tbl)
.where(
lf.op.and(
tbl.col("remote_key").eq(remoteKey),
tbl.col("remote_mtime").eq(remoteMTime),
tbl.col("remote_extra_key").eq(remoteExtraKey),
tbl.col("remote_type").eq("s3")
)
)
.exec()) as SyncMetaMappingRecord[];
if (res.length === 1) { if (potentialItem === null) {
return res[0]; // no result was found
}
if (res.length === 0) {
return undefined; return undefined;
} }
throw Error("something bad in sync meta mapping!"); if (
potentialItem.remoteKey === remoteKey &&
potentialItem.remoteMtime === remoteMTime &&
potentialItem.remoteExtraKey === remoteExtraKey &&
potentialItem.remoteType === "s3"
) {
// the result was found
return potentialItem;
} else {
return undefined;
}
}; };
export const clearAllSyncMetaMapping = async (db: lf.DatabaseConnection) => { export const clearAllSyncMetaMapping = async (db: InternalDBs) => {
const tbl = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING); await db.syncMappingTbl.clear();
await db.delete().from(tbl).exec();
}; };
export const insertSyncPlanRecord = async ( export const insertSyncPlanRecord = async (
db: lf.DatabaseConnection, db: InternalDBs,
syncPlan: SyncPlanType syncPlan: SyncPlanType
) => { ) => {
const schema = db.getSchema().table(DEFAULT_SYNC_PLANS_HISTORY); const record = {
const row = schema.createRow({
ts: syncPlan.ts, ts: syncPlan.ts,
remote_type: syncPlan.remoteType, remoteType: syncPlan.remoteType,
sync_plan: JSON.stringify(syncPlan, null, 2), syncPlan: JSON.stringify(syncPlan /* directly stringify */, null, 2),
} as SyncPlanRecord); } as SyncPlanRecord;
await db.insertOrReplace().into(schema).values([row]).exec(); await db.syncPlansTbl.setItem(`${syncPlan.ts}`, record);
}; };
export const clearAllSyncPlanRecords = async (db: lf.DatabaseConnection) => { export const clearAllSyncPlanRecords = async (db: InternalDBs) => {
const tbl = db.getSchema().table(DEFAULT_SYNC_PLANS_HISTORY); await db.syncPlansTbl.clear();
await db.delete().from(tbl).exec();
}; };
export const readAllSyncPlanRecordTexts = async (db: lf.DatabaseConnection) => { export const readAllSyncPlanRecordTexts = async (db: InternalDBs) => {
const schema = db.getSchema().table(DEFAULT_SYNC_PLANS_HISTORY); const records = [] as SyncPlanRecord[];
await db.syncPlansTbl.iterate((value, key, iterationNumber) => {
const records = (await db records.push(value as SyncPlanRecord);
.select() });
.from(schema) records.sort((a, b) => -(a.ts - b.ts)); // descending
.orderBy(schema.col("ts"), lf.Order.DESC)
.exec()) as SyncPlanRecord[];
if (records === undefined) { if (records === undefined) {
return [] as string[]; return [] as string[];
} else { } else {
return records.map((x) => x.sync_plan); return records.map((x) => x.syncPlan);
} }
}; };

View File

@ -11,20 +11,17 @@ import {
TFolder, TFolder,
} from "obsidian"; } from "obsidian";
import * as CodeMirror from "codemirror"; import * as CodeMirror from "codemirror";
import {
clearAllSyncPlanRecords,
clearAllSyncMetaMapping,
DatabaseConnection,
} from "./localdb";
import { import {
prepareDBs, prepareDBs,
destroyDBs, destroyDBs,
loadDeleteRenameHistoryTable, loadDeleteRenameHistoryTable,
clearAllSyncPlanRecords,
clearAllSyncMetaMapping,
insertDeleteRecord, insertDeleteRecord,
insertRenameRecord, insertRenameRecord,
getAllDeleteRenameRecords,
insertSyncPlanRecord, insertSyncPlanRecord,
} from "./localdb"; } from "./localdb";
import type { InternalDBs } from "./localdb";
import type { SyncStatusType, PasswordCheckType } from "./sync"; import type { SyncStatusType, PasswordCheckType } from "./sync";
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync"; import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
@ -50,7 +47,7 @@ const DEFAULT_SETTINGS: SaveRemotePluginSettings = {
export default class SaveRemotePlugin extends Plugin { export default class SaveRemotePlugin extends Plugin {
settings: SaveRemotePluginSettings; settings: SaveRemotePluginSettings;
cm: CodeMirror.Editor; cm: CodeMirror.Editor;
db: DatabaseConnection; db: InternalDBs;
syncStatus: SyncStatusType; syncStatus: SyncStatusType;
async onload() { async onload() {

View File

@ -1,14 +1,14 @@
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian"; import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
import { S3Client } from "@aws-sdk/client-s3"; import { S3Client } from "@aws-sdk/client-s3";
import * as lf from "lovefield-ts/dist/es6/lf.js";
import { import {
clearDeleteRenameHistoryOfKey, clearDeleteRenameHistoryOfKey,
FileFolderHistoryRecord,
upsertSyncMetaMappingDataS3, upsertSyncMetaMappingDataS3,
getSyncMetaMappingByRemoteKeyS3, getSyncMetaMappingByRemoteKeyS3,
} from "./localdb"; } from "./localdb";
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
import { import {
S3Config, S3Config,
S3ObjectType, S3ObjectType,
@ -146,7 +146,7 @@ const ensembleMixedStates = async (
remote: S3ObjectType[], remote: S3ObjectType[],
local: TAbstractFile[], local: TAbstractFile[],
deleteHistory: FileFolderHistoryRecord[], deleteHistory: FileFolderHistoryRecord[],
db: lf.DatabaseConnection, db: InternalDBs,
password: string = "" password: string = ""
) => { ) => {
const results = {} as Record<string, FileOrFolderMixedState>; const results = {} as Record<string, FileOrFolderMixedState>;
@ -167,12 +167,12 @@ const ensembleMixedStates = async (
let r = {} as FileOrFolderMixedState; let r = {} as FileOrFolderMixedState;
if (backwardMapping !== undefined) { if (backwardMapping !== undefined) {
key = backwardMapping.local_key; key = backwardMapping.localKey;
r = { r = {
key: key, key: key,
exist_remote: true, exist_remote: true,
mtime_remote: backwardMapping.local_mtime, mtime_remote: backwardMapping.localMtime,
size_remote: backwardMapping.local_size, size_remote: backwardMapping.localSize,
remote_encrypted_key: remoteEncryptedKey, remote_encrypted_key: remoteEncryptedKey,
}; };
} else { } else {
@ -240,11 +240,11 @@ const ensembleMixedStates = async (
for (const entry of deleteHistory) { for (const entry of deleteHistory) {
let key = entry.key; let key = entry.key;
if (entry.key_type === "folder") { if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) { if (!entry.key.endsWith("/")) {
key = `${entry.key}/`; key = `${entry.key}/`;
} }
} else if (entry.key_type === "file") { } else if (entry.keyType === "file") {
// pass // pass
} else { } else {
throw Error(`unexpected ${entry}`); throw Error(`unexpected ${entry}`);
@ -252,7 +252,7 @@ const ensembleMixedStates = async (
const r = { const r = {
key: key, key: key,
delete_time_local: entry.action_when, delete_time_local: entry.actionWhen,
} as FileOrFolderMixedState; } as FileOrFolderMixedState;
if (isHiddenPath(key)) { if (isHiddenPath(key)) {
@ -405,7 +405,7 @@ export const getSyncPlan = async (
remote: S3ObjectType[], remote: S3ObjectType[],
local: TAbstractFile[], local: TAbstractFile[],
deleteHistory: FileFolderHistoryRecord[], deleteHistory: FileFolderHistoryRecord[],
db: lf.DatabaseConnection, db: InternalDBs,
password: string = "" password: string = ""
) => { ) => {
const mixedStates = await ensembleMixedStates( const mixedStates = await ensembleMixedStates(
@ -429,7 +429,7 @@ export const getSyncPlan = async (
export const doActualSync = async ( export const doActualSync = async (
s3Client: S3Client, s3Client: S3Client,
s3Config: S3Config, s3Config: S3Config,
db: lf.DatabaseConnection, db: InternalDBs,
vault: Vault, vault: Vault,
syncPlan: SyncPlanType, syncPlan: SyncPlanType,
password: string = "" password: string = ""