Compare commits

..
13 Commits
Author SHA1 Message Date
fyears b13bd8708b bump to 0.3.14 2022-03-27 00:55:12 +08:00
fyears 83c3aac317 some logic to move away vaultRandomID from data.json 2022-03-27 00:52:10 +08:00
fyears 712979fb0d reset depth settings of webdav 2022-03-26 22:58:57 +08:00
fyears e460c563af skip extra checking folders for webdav 2022-03-26 22:41:10 +08:00
fyears 580397fbf9 update text 2022-03-26 15:11:41 +08:00
fyears 537fae1a2b add tracking for local movement 2022-03-26 15:06:34 +08:00
fyears dbc9e02ff3 update bug report tmpl 2022-03-26 11:24:20 +08:00
fyears 6474faad51 fix memory. Squashed commit of the following:
commit 1c24e48f2528238aeac417866ccec9ee3deced5f
Author: fyears <1142836+fyears@users.noreply.github.com>
Date:   Fri Mar 25 02:08:05 2022 +0800

    just some trying
2022-03-26 09:45:48 +08:00
fyears a5e415aeaf Merge branch 'master' of https://github.com/fyears/remotely-save 2022-03-26 00:34:13 +08:00
fyears 3e75e31696 change wording in config file 2022-03-26 00:34:00 +08:00
fyears 9c8cc1c8b8 update esbuild 2022-03-25 23:41:29 +08:00
fyears cccc4969cd Update browser_env.md 2022-03-23 23:38:48 +08:00
fyears 1ba1591c75 Update README.md 2022-03-23 23:37:49 +08:00
14 changed files with 408 additions and 132 deletions
+9 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: File a bug report description: Fire a bug report
title: "[Bug]: " title: "[Bug]: "
labels: ["bug"] labels: ["bug"]
body: body:
@@ -55,6 +55,14 @@ body:
description: What version of Obsidian are you running? description: What version of Obsidian are you running?
validations: validations:
required: false required: false
- type: checkboxes
id: using-password
attributes:
label: Using password or not
description: Are you using password (end-to-end encryption) or not?
options:
- label: Yes.
required: false
- type: checkboxes - type: checkboxes
id: ensure-no-sensitive-information id: ensure-no-sensitive-information
attributes: attributes:
+2 -2
View File
@@ -87,8 +87,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### webdav ### webdav
- About CORS: - About CORS:
- If you are using Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, you can skip this CORS part. - If you are using Obsidian desktop >= 0.13.25 or iOS >= 1.1.1, you can skip this CORS part.
- If you are using Obsidian desktop < 0.13.25 or mobile < 1.1.1: - If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version. - The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version. - Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details. - The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
+1 -1
View File
@@ -6,7 +6,7 @@ Technically, the plugin (or any plugin?) runs in the js environment provided by
Then some limitations are applied: Then some limitations are applied:
1. [The CORS issue (solved in the new Obsidian version).](./browser_env_cors.md) 1. [The CORS issue (solved in the new Obsidian version on some platforms).](./browser_env_cors.md)
2. [No Node.js environment.](./browser_env_no_nodejs.md) 2. [No Node.js environment.](./browser_env_no_nodejs.md)
3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md). 3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md).
4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md) 4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md)
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.3.13", "version": "0.3.14",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.3.13", "version": "0.3.14",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs", "dev2": "node esbuild.config.mjs",
@@ -38,7 +38,7 @@
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "^0.14.3", "esbuild": "^0.14.27",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
@@ -65,7 +65,6 @@
"assert": "^2.0.0", "assert": "^2.0.0",
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"codemirror": "^5.63.1",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"dropbox": "^10.22.0", "dropbox": "^10.22.0",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
+7 -2
View File
@@ -66,7 +66,6 @@ export interface RemotelySavePluginSettings {
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;
currLogLevel?: string; currLogLevel?: string;
vaultRandomID?: string;
autoRunEveryMilliseconds?: number; autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number; initRunAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean; agreeToUploadExtraMetadata?: boolean;
@@ -74,6 +73,11 @@ export interface RemotelySavePluginSettings {
syncConfigDir?: boolean; syncConfigDir?: boolean;
syncUnderscoreItems?: boolean; syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto; lang?: LangTypeAndAuto;
/**
* @deprecated
*/
vaultRandomID?: string;
} }
export interface RemoteItem { export interface RemoteItem {
@@ -124,7 +128,8 @@ export interface FileOrFolderMixedState {
deltimeRemote?: number; deltimeRemote?: number;
sizeLocal?: number; sizeLocal?: number;
sizeRemote?: number; sizeRemote?: number;
changeMtimeUsingMapping?: boolean; changeRemoteMtimeUsingMapping?: boolean;
changeLocalMtimeUsingMapping?: boolean;
decision?: DecisionType; decision?: DecisionType;
decisionBranch?: number; decisionBranch?: number;
syncDone?: "done"; syncDone?: "done";
+1 -1
View File
@@ -7,7 +7,7 @@ import * as origLog from "loglevel";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DEFAULT_README: string = const DEFAULT_README: string =
"Do NOT modify this manually. It's generated automatically."; "The file contains sensitive info, so DO NOT take screenshot of, copy, or share it to anyone! It's also generated automatically, so do not edit it manually.";
interface MessyConfigType { interface MessyConfigType {
readme: string; readme: string;
+163 -33
View File
@@ -1,21 +1,23 @@
import localforage from "localforage"; import localforage from "localforage";
import { TAbstractFile, TFile, TFolder } from "obsidian"; import { requireApiVersion, TAbstractFile, TFile, TFolder } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import { API_VER_STAT_FOLDER, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
import type { SyncPlanType } from "./sync"; import type { SyncPlanType } from "./sync";
export type LocalForage = typeof localforage; export type LocalForage = typeof localforage;
import * as origLog from "loglevel"; import * as origLog from "loglevel";
import { nanoid } from "nanoid";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108]; const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326];
export const DEFAULT_DB_VERSION_NUMBER: number = 20220108; export const DEFAULT_DB_VERSION_NUMBER: number = 20220326;
export const DEFAULT_DB_NAME = "remotelysavedb"; export const DEFAULT_DB_NAME = "remotelysavedb";
export const DEFAULT_TBL_VERSION = "schemaversion"; export const DEFAULT_TBL_VERSION = "schemaversion";
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory"; export const DEFAULT_TBL_FILE_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";
export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
export interface FileFolderHistoryRecord { export interface FileFolderHistoryRecord {
key: string; key: string;
@@ -23,7 +25,7 @@ export interface FileFolderHistoryRecord {
mtime: number; mtime: number;
size: number; size: number;
actionWhen: number; actionWhen: number;
actionType: "delete" | "rename"; actionType: "delete" | "rename" | "renameDestination";
keyType: "folder" | "file"; keyType: "folder" | "file";
renameTo: string; renameTo: string;
vaultRandomID: string; vaultRandomID: string;
@@ -51,9 +53,10 @@ interface SyncPlanRecord {
export interface InternalDBs { export interface InternalDBs {
versionTbl: LocalForage; versionTbl: LocalForage;
deleteHistoryTbl: LocalForage; fileHistoryTbl: LocalForage;
syncMappingTbl: LocalForage; syncMappingTbl: LocalForage;
syncPlansTbl: LocalForage; syncPlansTbl: LocalForage;
vaultRandomIDMappingTbl: LocalForage;
} }
/** /**
@@ -72,12 +75,12 @@ const migrateDBsFrom20211114To20220108 = async (
const allPromisesToWait: Promise<any>[] = []; const allPromisesToWait: Promise<any>[] = [];
log.debug("assign vault id to any delete history"); log.debug("assign vault id to any delete history");
const keysInDeleteHistoryTbl = await db.deleteHistoryTbl.keys(); const keysInDeleteHistoryTbl = await db.fileHistoryTbl.keys();
for (const key of keysInDeleteHistoryTbl) { for (const key of keysInDeleteHistoryTbl) {
if (key.startsWith(vaultRandomID)) { if (key.startsWith(vaultRandomID)) {
continue; continue;
} }
const value = (await db.deleteHistoryTbl.getItem( const value = (await db.fileHistoryTbl.getItem(
key key
)) as FileFolderHistoryRecord; )) as FileFolderHistoryRecord;
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -87,8 +90,8 @@ const migrateDBsFrom20211114To20220108 = async (
value.vaultRandomID = vaultRandomID; value.vaultRandomID = vaultRandomID;
} }
const newKey = `${vaultRandomID}\t${key}`; const newKey = `${vaultRandomID}\t${key}`;
allPromisesToWait.push(db.deleteHistoryTbl.setItem(newKey, value)); allPromisesToWait.push(db.fileHistoryTbl.setItem(newKey, value));
allPromisesToWait.push(db.deleteHistoryTbl.removeItem(key)); allPromisesToWait.push(db.fileHistoryTbl.removeItem(key));
} }
log.debug("assign vault id to any sync mapping"); log.debug("assign vault id to any sync mapping");
@@ -136,6 +139,23 @@ const migrateDBsFrom20211114To20220108 = async (
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`); log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
}; };
/**
* no need to do anything except changing version
* we just add more file operations in db, and no schema is changed.
* @param db
* @param vaultRandomID
*/
const migrateDBsFrom20220108To20220326 = async (
db: InternalDBs,
vaultRandomID: string
) => {
const oldVer = 20220108;
const newVer = 20220326;
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
await db.versionTbl.setItem("version", newVer);
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
};
const migrateDBs = async ( const migrateDBs = async (
db: InternalDBs, db: InternalDBs,
oldVer: number, oldVer: number,
@@ -148,19 +168,36 @@ const migrateDBs = async (
if (oldVer === 20211114 && newVer === 20220108) { if (oldVer === 20211114 && newVer === 20220108) {
return await migrateDBsFrom20211114To20220108(db, vaultRandomID); return await migrateDBsFrom20211114To20220108(db, vaultRandomID);
} }
if (oldVer === 20220108 && newVer === 20220326) {
return await migrateDBsFrom20220108To20220326(db, vaultRandomID);
}
if (oldVer === 20211114 && newVer === 20220326) {
// TODO: more steps with more versions in the future
await migrateDBsFrom20211114To20220108(db, vaultRandomID);
await migrateDBsFrom20220108To20220326(db, vaultRandomID);
return;
}
if (newVer < oldVer) {
throw Error(
"You've installed a new version, but then downgrade to an old version. Stop working!"
);
}
// not implemented // not implemented
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`); throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
}; };
export const prepareDBs = async (vaultRandomID: string) => { export const prepareDBs = async (
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) => {
const db = { const db = {
versionTbl: localforage.createInstance({ versionTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VERSION, storeName: DEFAULT_TBL_VERSION,
}), }),
deleteHistoryTbl: localforage.createInstance({ fileHistoryTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_DELETE_HISTORY, storeName: DEFAULT_TBL_FILE_HISTORY,
}), }),
syncMappingTbl: localforage.createInstance({ syncMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
@@ -170,9 +207,42 @@ export const prepareDBs = async (vaultRandomID: string) => {
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_SYNC_PLANS_HISTORY, storeName: DEFAULT_SYNC_PLANS_HISTORY,
}), }),
vaultRandomIDMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING,
}),
} as InternalDBs; } as InternalDBs;
const originalVersion = (await db.versionTbl.getItem("version")) as number; // try to get vaultRandomID firstly
let vaultRandomID = "";
const vaultRandomIDInDB: string | null =
await db.vaultRandomIDMappingTbl.getItem(`path2id\t${vaultBasePath}`);
if (vaultRandomIDInDB === null) {
if (vaultRandomIDFromOldConfigFile !== "") {
// reuse the old config id
vaultRandomID = vaultRandomIDFromOldConfigFile;
} else {
// no old config id, we create a random one
vaultRandomID = nanoid();
}
// save the id back
await db.vaultRandomIDMappingTbl.setItem(
`path2id\t${vaultBasePath}`,
vaultRandomID
);
await db.vaultRandomIDMappingTbl.setItem(
`id2path\t${vaultRandomID}`,
vaultBasePath
);
} else {
vaultRandomID = vaultRandomIDInDB;
}
if (vaultRandomID === "") {
throw Error("no vaultRandomID found or generated");
}
const originalVersion: number | null = await db.versionTbl.getItem("version");
if (originalVersion === null) { if (originalVersion === null) {
log.debug( log.debug(
`no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}` `no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}`
@@ -193,7 +263,10 @@ export const prepareDBs = async (vaultRandomID: string) => {
} }
log.info("db connected"); log.info("db connected");
return db; return {
db: db,
vaultRandomID: vaultRandomID,
};
}; };
export const destroyDBs = async () => { export const destroyDBs = async () => {
@@ -206,20 +279,20 @@ export const destroyDBs = async () => {
log.info("db deleted"); log.info("db deleted");
}; };
req.onblocked = (event) => { req.onblocked = (event) => {
console.warn("trying to delete db but it was blocked"); log.warn("trying to delete db but it was blocked");
}; };
req.onerror = (event) => { req.onerror = (event) => {
console.error("tried to delete db but something bad!"); log.error("tried to delete db but something goes wrong!");
console.error(event); log.error(event);
}; };
}; };
export const loadDeleteRenameHistoryTableByVault = async ( export const loadFileHistoryTableByVault = async (
db: InternalDBs, db: InternalDBs,
vaultRandomID: string vaultRandomID: string
) => { ) => {
const records = [] as FileFolderHistoryRecord[]; const records = [] as FileFolderHistoryRecord[];
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => { await db.fileHistoryTbl.iterate((value, key, iterationNumber) => {
if (key.startsWith(`${vaultRandomID}\t`)) { if (key.startsWith(`${vaultRandomID}\t`)) {
records.push(value as FileFolderHistoryRecord); records.push(value as FileFolderHistoryRecord);
} }
@@ -233,7 +306,16 @@ export const clearDeleteRenameHistoryOfKeyAndVault = async (
key: string, key: string,
vaultRandomID: string vaultRandomID: string
) => { ) => {
await db.deleteHistoryTbl.removeItem(`${vaultRandomID}\t${key}`); const fullKey = `${vaultRandomID}\t${key}`;
const item: FileFolderHistoryRecord | null = await db.fileHistoryTbl.getItem(
fullKey
);
if (
item !== null &&
(item.actionType === "delete" || item.actionType === "rename")
) {
await db.fileHistoryTbl.removeItem(fullKey);
}
}; };
export const insertDeleteRecordByVault = async ( export const insertDeleteRecordByVault = async (
@@ -260,10 +342,12 @@ export const insertDeleteRecordByVault = async (
const key = fileOrFolder.path.endsWith("/") const key = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
const ctime = 0; // they are deleted, so no way to get ctime, mtime
const mtime = 0; // they are deleted, so no way to get ctime, mtime
k = { k = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: Date.now(),
actionType: "delete", actionType: "delete",
@@ -272,9 +356,19 @@ export const insertDeleteRecordByVault = async (
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
}; };
/**
* A file/folder is renamed from A to B
* We insert two records:
* A with actionType="rename"
* B with actionType="renameDestination"
* @param db
* @param fileOrFolder
* @param oldPath
* @param vaultRandomID
*/
export const insertRenameRecordByVault = async ( export const insertRenameRecordByVault = async (
db: InternalDBs, db: InternalDBs,
fileOrFolder: TAbstractFile, fileOrFolder: TAbstractFile,
@@ -282,37 +376,73 @@ export const insertRenameRecordByVault = async (
vaultRandomID: string vaultRandomID: string
) => { ) => {
// log.info(fileOrFolder); // log.info(fileOrFolder);
let k: FileFolderHistoryRecord; let k1: FileFolderHistoryRecord;
let k2: FileFolderHistoryRecord;
const actionWhen = Date.now();
if (fileOrFolder instanceof TFile) { if (fileOrFolder instanceof TFile) {
k = { k1 = {
key: oldPath, key: oldPath,
ctime: fileOrFolder.stat.ctime, ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime, mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size, size: fileOrFolder.stat.size,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "file", keyType: "file",
renameTo: fileOrFolder.path, renameTo: fileOrFolder.path,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: fileOrFolder.path,
ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "file",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} else if (fileOrFolder instanceof TFolder) { } else if (fileOrFolder instanceof TFolder) {
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`; const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
const renameTo = fileOrFolder.path.endsWith("/") const renameTo = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
k = { let ctime = 0;
let mtime = 0;
if (requireApiVersion(API_VER_STAT_FOLDER)) {
// TAbstractFile does not contain these info
// but from API_VER_STAT_FOLDER we can manually stat them by path.
const s = await fileOrFolder.vault.adapter.stat(fileOrFolder.path);
ctime = s.ctime;
mtime = s.mtime;
}
k1 = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "folder", keyType: "folder",
renameTo: renameTo, renameTo: renameTo,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: renameTo,
ctime: ctime,
mtime: mtime,
size: 0,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "folder",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await Promise.all([
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1.key}`, k1),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2.key}`, k2),
]);
}; };
export const upsertSyncMetaMappingDataByVault = async ( export const upsertSyncMetaMappingDataByVault = async (
+95 -41
View File
@@ -1,6 +1,13 @@
import { Modal, Notice, Plugin, Setting, addIcon, setIcon } from "obsidian"; import {
Modal,
Notice,
Plugin,
Setting,
addIcon,
setIcon,
FileSystemAdapter,
} from "obsidian";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { nanoid } from "nanoid";
import { createElement, RotateCcw, RefreshCcw } from "lucide"; import { createElement, RotateCcw, RefreshCcw } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes"; import type { RemotelySavePluginSettings } from "./baseTypes";
import { import {
@@ -10,13 +17,13 @@ import {
COMMAND_URI, COMMAND_URI,
} from "./baseTypes"; } from "./baseTypes";
import { importQrCodeUri } from "./importExport"; import { importQrCodeUri } from "./importExport";
import type { InternalDBs } from "./localdb";
import { import {
insertDeleteRecordByVault, insertDeleteRecordByVault,
insertRenameRecordByVault, insertRenameRecordByVault,
insertSyncPlanRecordByVault, insertSyncPlanRecordByVault,
loadDeleteRenameHistoryTableByVault, loadFileHistoryTableByVault,
prepareDBs, prepareDBs,
InternalDBs,
} from "./localdb"; } from "./localdb";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { import {
@@ -54,7 +61,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
password: "", password: "",
serviceType: "s3", serviceType: "s3",
currLogLevel: "info", currLogLevel: "info",
vaultRandomID: "", // vaultRandomID: "", // deprecated
autoRunEveryMilliseconds: -1, autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1, initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false, agreeToUploadExtraMetadata: false,
@@ -77,16 +84,25 @@ type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`; const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`; const iconNameSyncRunning = `remotely-save-sync-running`;
const iconSvgSyncWait = createElement(RotateCcw); const getIconSvg = () => {
iconSvgSyncWait.setAttribute("width", "100"); const iconSvgSyncWait = createElement(RotateCcw);
iconSvgSyncWait.setAttribute("height", "100"); iconSvgSyncWait.setAttribute("width", "100");
const iconSvgSyncRunning = createElement(RefreshCcw); iconSvgSyncWait.setAttribute("height", "100");
iconSvgSyncRunning.setAttribute("width", "100"); const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("height", "100"); iconSvgSyncRunning.setAttribute("width", "100");
iconSvgSyncRunning.setAttribute("height", "100");
const res = {
iconSvgSyncWait: iconSvgSyncWait.outerHTML,
iconSvgSyncRunning: iconSvgSyncRunning.outerHTML,
};
iconSvgSyncWait.empty();
iconSvgSyncRunning.empty();
return res;
};
export default class RemotelySavePlugin extends Plugin { export default class RemotelySavePlugin extends Plugin {
settings: RemotelySavePluginSettings; settings: RemotelySavePluginSettings;
// cm: CodeMirror.Editor;
db: InternalDBs; db: InternalDBs;
syncStatus: SyncStatusType; syncStatus: SyncStatusType;
oauth2Info: OAuth2Info; oauth2Info: OAuth2Info;
@@ -95,6 +111,7 @@ export default class RemotelySavePlugin extends Plugin {
syncRibbon?: HTMLElement; syncRibbon?: HTMLElement;
autoRunIntervalID?: number; autoRunIntervalID?: number;
i18n: I18n; i18n: I18n;
vaultRandomID: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") { async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
const t = (x: TransItemType, vars?: any) => { const t = (x: TransItemType, vars?: any) => {
@@ -207,7 +224,7 @@ export default class RemotelySavePlugin extends Plugin {
const { remoteStates, metadataFile } = await parseRemoteItems( const { remoteStates, metadataFile } = await parseRemoteItems(
remoteRsp.Contents, remoteRsp.Contents,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
client.serviceType, client.serviceType,
this.settings.password this.settings.password
); );
@@ -225,9 +242,9 @@ export default class RemotelySavePlugin extends Plugin {
); );
this.syncStatus = "getting_local_meta"; this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles(); const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadDeleteRenameHistoryTableByVault( const localHistory = await loadFileHistoryTableByVault(
this.db, this.db,
this.settings.vaultRandomID this.vaultRandomID
); );
let localConfigDirContents: ObsConfigDirFileType[] = undefined; let localConfigDirContents: ObsConfigDirFileType[] = undefined;
if (this.settings.syncConfigDir) { if (this.settings.syncConfigDir) {
@@ -261,11 +278,7 @@ export default class RemotelySavePlugin extends Plugin {
); );
log.info(plan.mixedStates); // for debugging log.info(plan.mixedStates); // for debugging
if (triggerSource !== "dry") { if (triggerSource !== "dry") {
await insertSyncPlanRecordByVault( await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
this.db,
plan,
this.settings.vaultRandomID
);
} }
// The operations above are almost read only and kind of safe. // The operations above are almost read only and kind of safe.
@@ -282,7 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
await doActualSync( await doActualSync(
client, client,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
this.app.vault, this.app.vault,
plan, plan,
sortedKeys, sortedKeys,
@@ -344,8 +357,10 @@ export default class RemotelySavePlugin extends Plugin {
async onload() { async onload() {
log.info(`loading plugin ${this.manifest.id}`); log.info(`loading plugin ${this.manifest.id}`);
addIcon(iconNameSyncWait, iconSvgSyncWait.outerHTML); const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg();
addIcon(iconNameSyncRunning, iconSvgSyncRunning.outerHTML);
addIcon(iconNameSyncWait, iconSvgSyncWait);
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
this.oauth2Info = { this.oauth2Info = {
verifier: "", verifier: "",
@@ -373,12 +388,27 @@ export default class RemotelySavePlugin extends Plugin {
} }
await this.checkIfOauthExpires(); await this.checkIfOauthExpires();
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
// MUST before prepareDB()
// And, it's also possible to be an empty string,
// which means the vaultRandomID is read from db later!
const vaultRandomIDFromOldConfigFile =
await this.getVaultRandomIDFromOldConfigFile();
// no need to await this // no need to await this
this.tryToAddIgnoreFile(); this.tryToAddIgnoreFile();
await this.prepareDB(); const vaultBasePath = this.getVaultBasePath();
try {
await this.prepareDBAndVaultRandomID(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
} catch (err) {
new Notice(err.message, 10 * 1000);
throw err;
}
this.syncStatus = "idle"; this.syncStatus = "idle";
@@ -387,7 +417,7 @@ export default class RemotelySavePlugin extends Plugin {
await insertDeleteRecordByVault( await insertDeleteRecordByVault(
this.db, this.db,
fileOrFolder, fileOrFolder,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -398,7 +428,7 @@ export default class RemotelySavePlugin extends Plugin {
this.db, this.db,
fileOrFolder, fileOrFolder,
oldPath, oldPath,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -629,9 +659,13 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
onunload() { async onunload() {
log.info(`unloading plugin ${this.manifest.id}`); log.info(`unloading plugin ${this.manifest.id}`);
this.destroyDBs(); this.syncRibbon = undefined;
if (this.oauth2Info !== undefined) {
this.oauth2Info.helperModal = undefined;
this.oauth2Info = undefined;
}
} }
async loadSettings() { async loadSettings() {
@@ -737,14 +771,20 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async checkIfVaultIDAssigned() { async getVaultRandomIDFromOldConfigFile() {
if ( let vaultRandomID = "";
this.settings.vaultRandomID === undefined || if (this.settings.vaultRandomID !== undefined) {
this.settings.vaultRandomID === "" // In old version, the vault id is saved in data.json
) { // But we want to store it in localForage later
this.settings.vaultRandomID = nanoid(); if (this.settings.vaultRandomID !== "") {
// a real string was assigned before
vaultRandomID = this.settings.vaultRandomID;
}
log.debug("vaultRandomID is no longer saved in data.json");
delete this.settings.vaultRandomID;
await this.saveSettings(); await this.saveSettings();
} }
return vaultRandomID;
} }
async trash(x: string) { async trash(x: string) {
@@ -753,8 +793,26 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async prepareDB() { getVaultBasePath() {
this.db = await prepareDBs(this.settings.vaultRandomID); if (this.app.vault.adapter instanceof FileSystemAdapter) {
// in desktop
return this.app.vault.adapter.getBasePath().split("?")[0];
} else {
// in mobile
return this.app.vault.adapter.getResourcePath("").split("?")[0];
}
}
async prepareDBAndVaultRandomID(
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) {
const { db, vaultRandomID } = await prepareDBs(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
this.db = db;
this.vaultRandomID = vaultRandomID;
} }
enableAutoSyncIfSet() { enableAutoSyncIfSet() {
@@ -792,10 +850,6 @@ export default class RemotelySavePlugin extends Plugin {
await this.saveSettings(); await this.saveSettings();
} }
destroyDBs() {
/* destroyDBs(this.db); */
}
async setCurrSyncMsg( async setCurrSyncMsg(
i: number, i: number,
totalCount: number, totalCount: number,
+12 -10
View File
@@ -5,6 +5,8 @@ import {
requestUrl, requestUrl,
requireApiVersion, requireApiVersion,
Platform, Platform,
Notice,
RequestUrlResponse,
} from "obsidian"; } from "obsidian";
import { Queue } from "@fyears/tsqueue"; import { Queue } from "@fyears/tsqueue";
@@ -116,7 +118,6 @@ if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
// return r; // return r;
// }); // });
import { AuthType, BufferLike, createClient } from "webdav/web"; import { AuthType, BufferLike, createClient } from "webdav/web";
import { Plane } from "lucide";
export type { WebDAVClient } from "webdav/web"; export type { WebDAVClient } from "webdav/web";
export const DEFAULT_WEBDAV_CONFIG = { export const DEFAULT_WEBDAV_CONFIG = {
@@ -217,7 +218,7 @@ export class WrappedWebdavClient {
this.vaultFolderExists = true; this.vaultFolderExists = true;
} else { } else {
log.info("remote vault folder not exists, creating"); log.info("remote vault folder not exists, creating");
await this.client.createDirectory(`/${this.vaultName}`); await this.client.createDirectory(`/${this.vaultName}/`);
log.info("remote vault folder created!"); log.info("remote vault folder created!");
this.vaultFolderExists = true; this.vaultFolderExists = true;
} }
@@ -227,7 +228,7 @@ export class WrappedWebdavClient {
if (this.webdavConfig.depth === "auto_unknown") { if (this.webdavConfig.depth === "auto_unknown") {
let testPassed = false; let testPassed = false;
try { try {
const res = await this.client.customRequest(`/${this.vaultName}`, { const res = await this.client.customRequest(`/${this.vaultName}/`, {
method: "PROPFIND", method: "PROPFIND",
headers: { headers: {
Depth: "infinity", Depth: "infinity",
@@ -246,7 +247,7 @@ export class WrappedWebdavClient {
} }
if (!testPassed) { if (!testPassed) {
try { try {
const res = await this.client.customRequest(`/${this.vaultName}`, { const res = await this.client.customRequest(`/${this.vaultName}/`, {
method: "PROPFIND", method: "PROPFIND",
headers: { headers: {
Depth: "1", Depth: "1",
@@ -328,7 +329,7 @@ export const uploadToRemote = async (
if (password === "") { if (password === "") {
// if not encrypted, mkdir a remote folder // if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, { await client.client.createDirectory(uploadFile, {
recursive: true, recursive: false, // the sync algo should guarantee no need to recursive
}); });
const res = await getRemoteMeta(client, uploadFile); const res = await getRemoteMeta(client, uploadFile);
return res; return res;
@@ -360,11 +361,12 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await encryptArrayBuffer(localContent, password);
} }
// we need to create folders before uploading // updated 20220326: the algorithm guarantee this
const dir = getPathFolder(uploadFile); // // we need to create folders before uploading
if (dir !== "/" && dir !== "") { // const dir = getPathFolder(uploadFile);
await client.client.createDirectory(dir, { recursive: true }); // if (dir !== "/" && dir !== "") {
} // await client.client.createDirectory(dir, { recursive: false });
// }
await client.client.putFileContents(uploadFile, remoteContent, { await client.client.putFileContents(uploadFile, remoteContent, {
overwrite: true, overwrite: true,
onUploadProgress: (progress: any) => { onUploadProgress: (progress: any) => {
+41 -10
View File
@@ -1146,6 +1146,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.address) .setValue(this.plugin.settings.webdav.address)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.address = value.trim(); this.plugin.settings.webdav.address = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1159,6 +1165,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.username) .setValue(this.plugin.settings.webdav.username)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.username = value.trim(); this.plugin.settings.webdav.username = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1172,6 +1184,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.password) .setValue(this.plugin.settings.webdav.password)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.password = value.trim(); this.plugin.settings.webdav.password = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1376,15 +1394,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.onChange(async (val) => { .onChange(async (val) => {
if (val === "enable" && !bridge.secondConfirm) { if (val === "enable" && !bridge.secondConfirm) {
dropdown.setValue("disable"); dropdown.setValue("disable");
const modal = new SyncConfigDirModal( new SyncConfigDirModal(this.app, this.plugin, () => {
this.app, bridge.secondConfirm = true;
this.plugin, dropdown.setValue("enable");
() => { }).open();
bridge.secondConfirm = true;
dropdown.setValue("enable");
}
);
modal.open();
} else { } else {
bridge.secondConfirm = false; bridge.secondConfirm = false;
this.plugin.settings.syncConfigDir = false; this.plugin.settings.syncConfigDir = false;
@@ -1466,7 +1479,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await exportVaultSyncPlansToFiles( await exportVaultSyncPlansToFiles(
this.plugin.db, this.plugin.db,
this.app.vault, this.app.vault,
this.plugin.settings.vaultRandomID this.plugin.vaultRandomID
); );
new Notice(t("settings_syncplans_notice")); new Notice(t("settings_syncplans_notice"));
}); });
@@ -1494,6 +1507,18 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
const outputCurrBasePathVaultIDDiv = debugDiv.createDiv("div");
new Setting(outputCurrBasePathVaultIDDiv)
.setName(t("settings_outputbasepathvaultid"))
.setDesc(t("settings_outputbasepathvaultid_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_outputbasepathvaultid_button"));
button.onClick(async () => {
new Notice(this.plugin.getVaultBasePath());
new Notice(this.plugin.vaultRandomID);
});
});
const dbsResetDiv = debugDiv.createEl("div"); const dbsResetDiv = debugDiv.createEl("div");
new Setting(dbsResetDiv) new Setting(dbsResetDiv)
.setName(t("settings_resetcache")) .setName(t("settings_resetcache"))
@@ -1506,4 +1531,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
} }
hide() {
let { containerEl } = this;
containerEl.empty();
super.hide();
}
} }
+72 -25
View File
@@ -218,7 +218,7 @@ export const parseRemoteItems = async (
mtimeRemote: backwardMapping.localMtime || entry.lastModified, mtimeRemote: backwardMapping.localMtime || entry.lastModified,
sizeRemote: backwardMapping.localSize || entry.size, sizeRemote: backwardMapping.localSize || entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: true, changeRemoteMtimeUsingMapping: true,
}; };
} else { } else {
r = { r = {
@@ -227,7 +227,7 @@ export const parseRemoteItems = async (
mtimeRemote: entry.lastModified, mtimeRemote: entry.lastModified,
sizeRemote: entry.size, sizeRemote: entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: false, changeRemoteMtimeUsingMapping: false,
}; };
} }
@@ -295,7 +295,7 @@ const ensembleMixedStates = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
syncConfigDir: boolean, syncConfigDir: boolean,
configDir: string, configDir: string,
syncUnderscoreItems: boolean syncUnderscoreItems: boolean
@@ -397,7 +397,7 @@ const ensembleMixedStates = async (
} }
} }
for (const entry of localDeleteHistory) { for (const entry of localFileHistory) {
let key = entry.key; let key = entry.key;
if (entry.keyType === "folder") { if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) { if (!entry.key.endsWith("/")) {
@@ -409,23 +409,45 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`); throw Error(`unexpected ${entry}`);
} }
const r = {
key: key,
deltimeLocal: entry.actionWhen,
} as FileOrFolderMixedState;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) { if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue; continue;
} }
if (results.hasOwnProperty(key)) { if (entry.actionType === "delete" || entry.actionType === "rename") {
results[key].key = r.key; const r = {
results[key].deltimeLocal = r.deltimeLocal; key: key,
} else { deltimeLocal: entry.actionWhen,
results[key] = r; } as FileOrFolderMixedState;
results[key].existLocal = false; if (results.hasOwnProperty(key)) {
results[key].existRemote = false; results[key].deltimeLocal = r.deltimeLocal;
} else {
results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} else if (entry.actionType === "renameDestination") {
const r = {
key: key,
mtimeLocal: entry.actionWhen,
changeLocalMtimeUsingMapping: true,
};
if (results.hasOwnProperty(key)) {
results[key].mtimeLocal = Math.max(
r.mtimeLocal || 0,
results[key].mtimeLocal || 0
);
results[key].changeLocalMtimeUsingMapping =
r.changeLocalMtimeUsingMapping;
} else {
results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} else {
throw Error(
`do not know how to deal with local file history ${entry.key} with ${entry.actionType}`
);
} }
} }
@@ -480,12 +502,12 @@ const assignOperationToFileInplace = (
// 1. mtimeLocal // 1. mtimeLocal
if (r.existLocal) { if (r.existLocal) {
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeLocal >= mtimeRemote && r.mtimeLocal >= mtimeRemote &&
r.mtimeLocal >= deltimeLocal && r.mtimeLocal >= deltimeLocal &&
r.mtimeLocal >= deltime_remote r.mtimeLocal >= deltimeRemote
) { ) {
if (r.mtimeLocal === r.mtimeRemote) { if (r.mtimeLocal === r.mtimeRemote) {
// mtime the same // mtime the same
@@ -516,12 +538,12 @@ const assignOperationToFileInplace = (
// 2. mtimeRemote // 2. mtimeRemote
if (r.existRemote) { if (r.existRemote) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeRemote > mtimeLocal && r.mtimeRemote > mtimeLocal &&
r.mtimeRemote >= deltimeLocal && r.mtimeRemote >= deltimeLocal &&
r.mtimeRemote >= deltime_remote r.mtimeRemote >= deltimeRemote
) { ) {
r.decision = "downloadRemoteToLocal"; r.decision = "downloadRemoteToLocal";
r.decisionBranch = 5; r.decisionBranch = 5;
@@ -534,11 +556,11 @@ const assignOperationToFileInplace = (
if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) { if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
if ( if (
r.deltimeLocal >= mtimeLocal && r.deltimeLocal >= mtimeLocal &&
r.deltimeLocal >= mtimeRemote && r.deltimeLocal >= mtimeRemote &&
r.deltimeLocal >= deltime_remote r.deltimeLocal >= deltimeRemote
) { ) {
r.decision = "uploadLocalDelHistToRemote"; r.decision = "uploadLocalDelHistToRemote";
r.decisionBranch = 6; r.decisionBranch = 6;
@@ -549,7 +571,7 @@ const assignOperationToFileInplace = (
} }
} }
// 4. deltime_remote // 4. deltimeRemote
if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) { if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
@@ -617,6 +639,31 @@ const assignOperationToFolderInplace = async (
} }
} }
// If it was moved to here, after deletion, we should keep it as is.
// The logic not necessarily needs API_VER_STAT_FOLDER.
// The folder needs this logic because it's also determined by file children.
// But the file do not need this logic because the mtimeLocal is checked firstly.
if (
r.existLocal &&
r.changeLocalMtimeUsingMapping &&
r.mtimeLocal > 0 &&
r.mtimeLocal > deltimeLocal &&
r.mtimeLocal > deltimeRemote
) {
keptFolder.add(getParentFolder(r.key));
if (r.existLocal && r.existRemote) {
r.decision = "skipFolder";
r.decisionBranch = 16;
} else if (r.existLocal || r.existRemote) {
r.decision = "createFolder";
r.decisionBranch = 17;
} else {
throw Error(
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
);
}
}
if (r.decision === undefined) { if (r.decision === undefined) {
// not yet decided by the above reason // not yet decided by the above reason
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) { if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
@@ -678,7 +725,7 @@ export const getSyncPlan = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE, remoteType: SUPPORTED_SERVICES_TYPE,
vault: Vault, vault: Vault,
syncConfigDir: boolean, syncConfigDir: boolean,
@@ -691,7 +738,7 @@ export const getSyncPlan = async (
local, local,
localConfigDirContents, localConfigDirContents,
remoteDeleteHistory, remoteDeleteHistory,
localDeleteHistory, localFileHistory,
syncConfigDir, syncConfigDir,
configDir, configDir,
syncUnderscoreItems syncUnderscoreItems
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"0.3.2": "0.12.15", "0.3.2": "0.12.15",
"0.3.13": "0.13.21" "0.3.14": "0.13.21"
} }