Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59645e620a | ||
|
|
125da9d734 | ||
|
|
808ae709fb | ||
|
|
16c3c11fd8 | ||
|
|
be2b168c6c | ||
|
|
6b296d6110 | ||
|
|
6757348ac9 | ||
|
|
a7e6bed071 | ||
|
|
8b8c5bdb98 | ||
|
|
5b77b23e9f | ||
|
|
05313f9530 | ||
|
|
ce4e55fcc7 | ||
|
|
b26d6a7ffb |
@@ -38,3 +38,4 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
main.js
|
main.js
|
||||||
manifest.json
|
manifest.json
|
||||||
|
styles.css
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsdian-save-remote",
|
"id": "obsidian-save-remote",
|
||||||
"name": "Save remote",
|
"name": "Save remote",
|
||||||
"version": "0.0.12",
|
"version": "0.0.15",
|
||||||
"minAppVersion": "0.12.15",
|
"minAppVersion": "0.12.15",
|
||||||
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
|
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
|
||||||
"author": "fyears",
|
"author": "fyears",
|
||||||
|
|||||||
+5
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-save-remote",
|
"name": "obsidian-save-remote",
|
||||||
"version": "0.0.12",
|
"version": "0.0.15",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
@@ -35,12 +35,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.37.0",
|
"@aws-sdk/client-s3": "^3.37.0",
|
||||||
|
"@aws-sdk/lib-storage": "^3.40.1",
|
||||||
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
||||||
"acorn": "^8.5.0",
|
"acorn": "^8.5.0",
|
||||||
"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",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"webdav": "^4.7.0",
|
"webdav": "^4.7.0",
|
||||||
"webdav-fs": "^4.0.0"
|
"webdav-fs": "^4.0.0",
|
||||||
|
"xregexp": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-11
@@ -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);
|
||||||
|
|||||||
+139
-171
@@ -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,84 +18,75 @@ 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({
|
||||||
|
// name: DEFAULT_DB_NAME,
|
||||||
|
// });
|
||||||
|
// console.log("db deleted");
|
||||||
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME);
|
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME);
|
||||||
req.onsuccess = (event) => {
|
req.onsuccess = (event) => {
|
||||||
console.log("db deleted");
|
console.log("db deleted");
|
||||||
@@ -107,37 +100,34 @@ export const destroyDBs = async (db: lf.DatabaseConnection) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadDeleteRenameHistoryTable = async (
|
const migrateDBs = async (db: InternalDBs, oldVer: number, newVer: number) => {
|
||||||
db: lf.DatabaseConnection
|
if (oldVer === newVer) {
|
||||||
) => {
|
return;
|
||||||
const schema = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
|
}
|
||||||
const tbl = db.getSchema().table(DEFAULT_TBL_DELETE_HISTORY);
|
// not implemented
|
||||||
|
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
|
||||||
|
};
|
||||||
|
|
||||||
const records = await db
|
export const loadDeleteRenameHistoryTable = async (db: InternalDBs) => {
|
||||||
.select()
|
const records = [] as FileFolderHistoryRecord[];
|
||||||
.from(schema)
|
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
|
||||||
.orderBy(schema.col("action_when"), lf.Order.ASC)
|
records.push(value as FileFolderHistoryRecord);
|
||||||
.exec();
|
});
|
||||||
|
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
|
||||||
return records as FileFolderHistoryRecord[];
|
return records;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +136,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 +151,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 +173,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 +188,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 +207,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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-25
@@ -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() {
|
||||||
@@ -105,6 +102,7 @@ export default class SaveRemotePlugin extends Plugin {
|
|||||||
this.settings.password
|
this.settings.password
|
||||||
);
|
);
|
||||||
if (!passwordCheckResult.ok) {
|
if (!passwordCheckResult.ok) {
|
||||||
|
new Notice("something goes wrong while checking password");
|
||||||
throw Error(passwordCheckResult.reason);
|
throw Error(passwordCheckResult.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,18 +198,20 @@ export class PasswordModal extends Modal {
|
|||||||
// contentEl.setText("Add Or change password.");
|
// contentEl.setText("Add Or change password.");
|
||||||
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
|
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "This password allows you encrypt your files before sending to remote services.",
|
text: "If the field is not empty, files are enctrypted using the password locally before sent to remote.",
|
||||||
|
});
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: "If the field is empty, then no password is used, and files would be sent without encryption.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", { text: "Empty means no password." });
|
|
||||||
|
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 1/4: The password setting itself is stored in PLAIN TEXT LOCALLY (because the plugin needs to use the password to encrypt the files) (and the password would not be sent to remote by this plugin).",
|
text: "Attention 1/4: The password itself is stored in PLAIN TEXT LOCALLY and would not be sent to remote by this plugin.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 2/4: The file contents are encrypted using openssl format. BUT, some metadata such as file sizes and directory structures are not encrypted or can be easily guessed.",
|
text: "Attention 2/4: Non-empty file contents are encrypted using openssl format. File/directory path are also encrypted then applied base32. BUT, some metadata such as file sizes and directory structures are not encrypted or can be easily guessed, and directory path are stored as 0-byte-size object remotely.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 3/4: If you change the password. You should make sure the remote service (s3/webdav/...) IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD. OTHERWISE SOMETHING BAD WOULD HAPPEN!",
|
text: "Attention 3/4: Before changing password, you should make sure the remote store (s3/webdav/...) IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD. OTHERWISE SOMETHING BAD WOULD HAPPEN!",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 4/4: The longer the password, the better.",
|
text: "Attention 4/4: The longer the password, the better.",
|
||||||
@@ -229,7 +229,7 @@ export class PasswordModal extends Modal {
|
|||||||
button.setClass("password_second_confirm");
|
button.setClass("password_second_confirm");
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText("Cancel (password not changed.)");
|
button.setButtonText("Go Back");
|
||||||
button.onClick(() => {
|
button.onClick(() => {
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
@@ -371,13 +371,10 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const syncPlanDiv = containerEl.createEl("div");
|
const syncPlanDiv = containerEl.createEl("div");
|
||||||
syncPlanDiv.createEl("p", {
|
syncPlanDiv.createEl("p", {
|
||||||
text: "Sync plans are created every time after you trigger sync and before the actual sync.",
|
text: "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync.",
|
||||||
});
|
|
||||||
syncPlanDiv.createEl("p", {
|
|
||||||
text: "They are useful to know what would actually happen in those sync.",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(syncPlanDiv)
|
||||||
.setName("export sync plans")
|
.setName("export sync plans")
|
||||||
.setDesc("export sync plans")
|
.setDesc("export sync plans")
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -388,7 +385,7 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(syncPlanDiv)
|
||||||
.setName("delete sync plans history in db")
|
.setName("delete sync plans history in db")
|
||||||
.setDesc("delete sync plans history in db")
|
.setDesc("delete sync plans history in db")
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -401,14 +398,10 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const syncMappingDiv = containerEl.createEl("div");
|
const syncMappingDiv = containerEl.createEl("div");
|
||||||
syncMappingDiv.createEl("p", {
|
syncMappingDiv.createEl("p", {
|
||||||
text: "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects.",
|
text: "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync.",
|
||||||
});
|
});
|
||||||
|
|
||||||
syncMappingDiv.createEl("p", {
|
new Setting(syncMappingDiv)
|
||||||
text: "If the sync mappings history are deleted, unnecessary data exchanges may occur in next-time syncing, because whether a remote object and local object with same name are equivalent or not could not be determined correctly by comparing last modified times.",
|
|
||||||
});
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName("delete sync mappings history in db")
|
.setName("delete sync mappings history in db")
|
||||||
.setDesc("delete sync mappings history in db")
|
.setDesc("delete sync mappings history in db")
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -418,5 +411,22 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
|||||||
new Notice("sync mappings history (in local db) deleted");
|
new Notice("sync mappings history (in local db) deleted");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dbsResetDiv = containerEl.createEl("div");
|
||||||
|
syncMappingDiv.createEl("p", {
|
||||||
|
text: "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings.",
|
||||||
|
});
|
||||||
|
new Setting(syncMappingDiv)
|
||||||
|
.setName("reset local internal cache/databases")
|
||||||
|
.setDesc("reset local internal cache/databases")
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Reset");
|
||||||
|
button.onClick(async () => {
|
||||||
|
await destroyDBs();
|
||||||
|
new Notice(
|
||||||
|
"Local internal cache/databases deleted. Please manually reload the plugin."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -2,6 +2,7 @@ import { Vault } from "obsidian";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { base32 } from "rfc4648";
|
import { base32 } from "rfc4648";
|
||||||
|
import XRegExp from "xregexp";
|
||||||
|
|
||||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp";
|
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp";
|
||||||
|
|
||||||
@@ -111,3 +112,21 @@ export const hexStringToTypedArray = (hex: string) => {
|
|||||||
export const base64ToBase32 = (a: string) => {
|
export const base64ToBase32 = (a: string) => {
|
||||||
return base32.stringify(Buffer.from(a, "base64"));
|
return base32.stringify(Buffer.from(a, "base64"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS Safari could decrypt string with invalid password!
|
||||||
|
* So we need an extra way to test the decrypted result.
|
||||||
|
* One simple way is testing the result are "valid", printable chars or not.
|
||||||
|
*
|
||||||
|
* https://stackoverflow.com/questions/6198986
|
||||||
|
* https://www.regular-expressions.info/unicode.html
|
||||||
|
* Manual test shows that emojis like '🍎' match '\\p{Cs}',
|
||||||
|
* so we need to write the regrex in a form that \p{C} minus \p{Cs}
|
||||||
|
* @param a
|
||||||
|
*/
|
||||||
|
export const isVaildText = (a: string) => {
|
||||||
|
// If the regex matches, the string is invalid.
|
||||||
|
return !XRegExp("\\p{Cc}|\\p{Cf}|\\p{Co}|\\p{Cn}|\\p{Zl}|\\p{Zp}", "A").test(
|
||||||
|
a
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Readable } from "stream";
|
|||||||
|
|
||||||
import { Vault } from "obsidian";
|
import { Vault } from "obsidian";
|
||||||
|
|
||||||
|
import { Upload } from "@aws-sdk/lib-storage";
|
||||||
import {
|
import {
|
||||||
S3Client,
|
S3Client,
|
||||||
ListObjectsV2Command,
|
ListObjectsV2Command,
|
||||||
@@ -115,14 +116,24 @@ export const uploadToRemote = async (
|
|||||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
remoteContent = await encryptArrayBuffer(localContent, password);
|
||||||
}
|
}
|
||||||
const body = arrayBufferToBuffer(remoteContent);
|
const body = arrayBufferToBuffer(remoteContent);
|
||||||
await s3Client.send(
|
|
||||||
new PutObjectCommand({
|
const upload = new Upload({
|
||||||
|
client: s3Client,
|
||||||
|
queueSize: 20, // concurrency
|
||||||
|
partSize: 5242880, // minimal 5MB by default
|
||||||
|
leavePartsOnError: false,
|
||||||
|
params: {
|
||||||
Bucket: s3Config.s3BucketName,
|
Bucket: s3Config.s3BucketName,
|
||||||
Key: uploadFile,
|
Key: uploadFile,
|
||||||
Body: body,
|
Body: body,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
})
|
},
|
||||||
);
|
});
|
||||||
|
upload.on("httpUploadProgress", (progress) => {
|
||||||
|
// console.log(progress);
|
||||||
|
});
|
||||||
|
await upload.done();
|
||||||
|
|
||||||
return await getRemoteMeta(s3Client, s3Config, uploadFile);
|
return await getRemoteMeta(s3Client, s3Config, uploadFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+32
-16
@@ -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,
|
||||||
@@ -16,7 +16,12 @@ import {
|
|||||||
deleteFromRemote,
|
deleteFromRemote,
|
||||||
downloadFromRemote,
|
downloadFromRemote,
|
||||||
} from "./s3";
|
} from "./s3";
|
||||||
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc";
|
import {
|
||||||
|
mkdirpInVault,
|
||||||
|
SUPPORTED_SERVICES_TYPE,
|
||||||
|
isHiddenPath,
|
||||||
|
isVaildText,
|
||||||
|
} from "./misc";
|
||||||
import {
|
import {
|
||||||
decryptBase32ToString,
|
decryptBase32ToString,
|
||||||
encryptStringToBase32,
|
encryptStringToBase32,
|
||||||
@@ -74,6 +79,7 @@ export interface PasswordCheckType {
|
|||||||
| "remote_encrypted_local_no_password"
|
| "remote_encrypted_local_no_password"
|
||||||
| "password_matched"
|
| "password_matched"
|
||||||
| "password_not_matched"
|
| "password_not_matched"
|
||||||
|
| "invalid_text_after_decryption"
|
||||||
| "remote_not_encrypted_local_has_password"
|
| "remote_not_encrypted_local_has_password"
|
||||||
| "no_password_both_sides";
|
| "no_password_both_sides";
|
||||||
}
|
}
|
||||||
@@ -101,10 +107,20 @@ export const isPasswordOk = async (
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await decryptBase32ToString(santyCheckKey, password);
|
const res = await decryptBase32ToString(santyCheckKey, password);
|
||||||
return {
|
|
||||||
ok: true,
|
// additional test
|
||||||
reason: "password_matched",
|
// because iOS Safari bypasses decryption with wrong password!
|
||||||
} as PasswordCheckType;
|
if (isVaildText(res)) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
reason: "password_matched",
|
||||||
|
} as PasswordCheckType;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "invalid_text_after_decryption",
|
||||||
|
} as PasswordCheckType;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -130,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>;
|
||||||
@@ -151,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 {
|
||||||
@@ -224,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}`);
|
||||||
@@ -236,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)) {
|
||||||
@@ -389,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(
|
||||||
@@ -413,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 = ""
|
||||||
|
|||||||
@@ -70,3 +70,22 @@ describe("Misc: get folder levels", () => {
|
|||||||
expect(misc.getFolderLevels(item3)).to.deep.equal(res3);
|
expect(misc.getFolderLevels(item3)).to.deep.equal(res3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Misc: vaild file name tests", () => {
|
||||||
|
it("should treat no ascii correctly", async () => {
|
||||||
|
const x = misc.isVaildText("😄🍎 apple 苹果");
|
||||||
|
// console.log(x)
|
||||||
|
expect(x).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find not-printable chars correctly", async () => {
|
||||||
|
const x = misc.isVaildText("😄🍎 apple 苹果\u0000");
|
||||||
|
// console.log(x)
|
||||||
|
expect(x).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow spaces/slashes/...", async () => {
|
||||||
|
const x = misc.isVaildText("😄🍎 apple 苹果/-_=/\\*%^&@#$`");
|
||||||
|
expect(x).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"0.0.12": "0.12.15"
|
"0.0.15": "0.12.15"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user