Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde4c9a294 | ||
|
|
7fc38a942a | ||
|
|
cc0c4fe224 | ||
|
|
2ab5640c12 | ||
|
|
b7a0376a0f | ||
|
|
d7d1c47e2a | ||
|
|
a65f5eb004 | ||
|
|
2ce34379ad | ||
|
|
40d2ee95b7 | ||
|
|
5632e28331 | ||
|
|
2b53a1bf5e | ||
|
|
ccd9e3116c | ||
|
|
e8d6a50793 |
+9
-9
@@ -15,15 +15,15 @@ Assuming all sources are reliable.
|
||||
We list all combinations mutually exclusive and collectively exhaustive.
|
||||
|
||||
| ID | Remote Files | Local files | Local delete rename history | Extra | Decision |
|
||||
| --- | ------------ | ----------- | --------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| --- | ------------ | ----------- | --------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | exist | exist | ignore | mtime_remote > mtime_local | download remote file, create local folder if not exists, clear local history if exists |
|
||||
| 2 | exist | exist | ignore | mtime_remote < mtime_local | upload local file, create remote folder if not exists, clear local history if exists |
|
||||
| 3 | exist | exist | ignore | mtime_remote === mtime_local && size_remote === size_local | clear local history if exists (the file was synced and no changes after last sync) |
|
||||
| 4 | exist | exist | ignore | mtime_remote === mtime_local && size_remote !== size_local | upload local file, clear local history if exists (we always prefer local to remote) |
|
||||
| 5 | exist | exist | ignore | If local is a folder. mtime_local === undefined | clear local history if exists. TODO: what if a folder and a previous file share the same name? |
|
||||
| 6 | exist | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists |
|
||||
| 7 | exist | not exist | exist | mtime_remote < delete_time_local | delete remote file, clear local history |
|
||||
| 8 | exist | not exist | not exist | | download remote file, create folder if not exists |
|
||||
| 9 | not exist | exist | ignore | If local is a single file. | upload local file, create remote folder if not exists, clear local history if exists |
|
||||
| 10 | not exist | exist | ignore | If local is a folder. | upload local files recursively, create remote folder if not exists, clear local history if exists |
|
||||
| 3 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote === size_local | clear local history if exists (the file was synced and no changes after last sync) |
|
||||
| 4 | exist | exist | ignore | mtime_remote === mtime_local && password === "" && size_remote !== size_local | upload local file, clear local history if exists (we always prefer local to remote) |
|
||||
| 5 | exist | exist | ignore | mtime_remote === mtime_local && password !== "" | clear local history if exists (in encryption mode, file sizes are unequal. we can only rely on mtime(s)) |
|
||||
| 6 | exist | exist | ignore | If local is a folder. mtime_local === undefined | clear local history if exists. TODO: what if a folder and a previous file share the same name? |
|
||||
| 7 | exist | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists |
|
||||
| 8 | exist | not exist | exist | mtime_remote < delete_time_local | delete remote file, clear local history |
|
||||
| 9 | exist | not exist | not exist | | download remote file, create folder if not exists |
|
||||
| 10 | not exist | exist | ignore | local may be folder or file | upload local files recursively, create remote folder if not exists, clear local history if exists |
|
||||
| 11 | not exist | not exist | ignore | | clear local history if exists |
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsdian-save-remote",
|
||||
"name": "Save remote",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.11",
|
||||
"minAppVersion": "0.12.15",
|
||||
"description": "This is yet another plugin allowing users to sync notes between local device and the cloud.",
|
||||
"author": "fyears",
|
||||
|
||||
+3
-2
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "obsidian-save-remote",
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.11",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"format": "npx prettier --write .",
|
||||
"clean": "npx rimraf main.js",
|
||||
"test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register 'tests/**/*.ts'"
|
||||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'"
|
||||
},
|
||||
"source": "main.ts",
|
||||
"keywords": [],
|
||||
@@ -19,6 +19,7 @@
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.37",
|
||||
"chai": "^4.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.4.1",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
|
||||
const DEFAULT_ITER = 10000;
|
||||
|
||||
// base32.stringify(Buffer.from('Salted__'))
|
||||
export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV";
|
||||
|
||||
const getKeyIVFromPassword = async (
|
||||
salt: Uint8Array,
|
||||
password: string,
|
||||
|
||||
@@ -276,6 +276,11 @@ export const getSyncMetaMappingByRemoteKeyS3 = async (
|
||||
throw Error("something bad in sync meta mapping!");
|
||||
};
|
||||
|
||||
export const clearAllSyncMetaMapping = async (db: lf.DatabaseConnection) => {
|
||||
const tbl = db.getSchema().table(DEFAULT_TBL_SYNC_MAPPING);
|
||||
await db.delete().from(tbl).exec();
|
||||
};
|
||||
|
||||
export const insertSyncPlanRecord = async (
|
||||
db: lf.DatabaseConnection,
|
||||
syncPlan: SyncPlanType
|
||||
|
||||
+143
-12
@@ -11,7 +11,11 @@ import {
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import * as CodeMirror from "codemirror";
|
||||
import { clearAllSyncPlanRecords, DatabaseConnection } from "./localdb";
|
||||
import {
|
||||
clearAllSyncPlanRecords,
|
||||
clearAllSyncMetaMapping,
|
||||
DatabaseConnection,
|
||||
} from "./localdb";
|
||||
import {
|
||||
prepareDBs,
|
||||
destroyDBs,
|
||||
@@ -22,9 +26,15 @@ import {
|
||||
insertSyncPlanRecord,
|
||||
} from "./localdb";
|
||||
|
||||
import type { SyncStatusType } from "./sync";
|
||||
import { getSyncPlan, doActualSync } from "./sync";
|
||||
import { DEFAULT_S3_CONFIG, getS3Client, listFromRemote, S3Config } from "./s3";
|
||||
import type { SyncStatusType, PasswordCheckType } from "./sync";
|
||||
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
|
||||
import {
|
||||
DEFAULT_S3_CONFIG,
|
||||
getS3Client,
|
||||
listFromRemote,
|
||||
S3Config,
|
||||
checkS3Connectivity,
|
||||
} from "./s3";
|
||||
import { exportSyncPlansToFiles } from "./debugMode";
|
||||
|
||||
interface SaveRemotePluginSettings {
|
||||
@@ -71,6 +81,7 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
}
|
||||
|
||||
try {
|
||||
//console.log(`huh ${this.settings.password}`)
|
||||
new Notice("1/6 Save Remote Sync Preparing");
|
||||
this.syncStatus = "preparing";
|
||||
|
||||
@@ -87,7 +98,17 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
// console.log(local);
|
||||
// console.log(localHistory);
|
||||
|
||||
new Notice("4/6 Starting to generate sync plan.");
|
||||
new Notice("4/7 Checking password correct or not.");
|
||||
this.syncStatus = "checking_password";
|
||||
const passwordCheckResult = await isPasswordOk(
|
||||
remoteRsp.Contents,
|
||||
this.settings.password
|
||||
);
|
||||
if (!passwordCheckResult.ok) {
|
||||
throw Error(passwordCheckResult.reason);
|
||||
}
|
||||
|
||||
new Notice("5/7 Starting to generate sync plan.");
|
||||
this.syncStatus = "generating_plan";
|
||||
const syncPlan = await getSyncPlan(
|
||||
remoteRsp.Contents,
|
||||
@@ -102,7 +123,7 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
// The operations above are read only and kind of safe.
|
||||
// The operations below begins to write or delete (!!!) something.
|
||||
|
||||
new Notice("5/6 Save Remote Sync data exchanging!");
|
||||
new Notice("6/7 Save Remote Sync data exchanging!");
|
||||
|
||||
this.syncStatus = "syncing";
|
||||
await doActualSync(
|
||||
@@ -114,7 +135,7 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
this.settings.password
|
||||
);
|
||||
|
||||
new Notice("6/6 Save Remote finish!");
|
||||
new Notice("7/7 Save Remote finish!");
|
||||
this.syncStatus = "finish";
|
||||
this.syncStatus = "idle";
|
||||
} catch (error) {
|
||||
@@ -122,7 +143,7 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
console.log(msg);
|
||||
console.log(error);
|
||||
new Notice(msg);
|
||||
new Notice(error);
|
||||
new Notice(error.message);
|
||||
this.syncStatus = "idle";
|
||||
}
|
||||
});
|
||||
@@ -165,6 +186,59 @@ export default class SaveRemotePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordModal extends Modal {
|
||||
plugin: SaveRemotePlugin;
|
||||
newPassword: string;
|
||||
constructor(app: App, plugin: SaveRemotePlugin, newPassword: string) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
let { contentEl } = this;
|
||||
// contentEl.setText("Add Or change password.");
|
||||
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
|
||||
contentEl.createEl("p", {
|
||||
text: "This password allows you encrypt your files before sending to remote services.",
|
||||
});
|
||||
contentEl.createEl("p", { text: "Empty means no password." });
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 1/3: 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).",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 2/3: 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!",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Attention 3/3: The longer the password, the better.",
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("The Second Confirm to change password.");
|
||||
button.onClick(async () => {
|
||||
this.plugin.settings.password = this.newPassword;
|
||||
await this.plugin.saveSettings();
|
||||
new Notice("New password saved!");
|
||||
this.close();
|
||||
});
|
||||
button.setClass("password_second_confirm");
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Cancel (password not changed.)");
|
||||
button.onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SaveRemoteSettingTab extends PluginSettingTab {
|
||||
plugin: SaveRemotePlugin;
|
||||
|
||||
@@ -247,22 +321,59 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("check connectivity")
|
||||
.setDesc("check connectivity")
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new Notice("Checking...");
|
||||
const s3Client = getS3Client(this.plugin.settings.s3);
|
||||
const res = await checkS3Connectivity(
|
||||
s3Client,
|
||||
this.plugin.settings.s3
|
||||
);
|
||||
if (res) {
|
||||
new Notice("Great! The bucket can be accessed.");
|
||||
} else {
|
||||
new Notice("The S3 bucket cannot be reached.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
containerEl.createEl("h2", { text: "General" });
|
||||
|
||||
let newPassword = `${this.plugin.settings.password}`;
|
||||
new Setting(containerEl)
|
||||
.setName("encryption password")
|
||||
.setDesc("Password for E2E encryption. Empty for no password.")
|
||||
.setDesc(
|
||||
'Password for E2E encryption. Empty for no password. You need to click "Confirm".'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(`${this.plugin.settings.password}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.password = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
newPassword = value.trim();
|
||||
})
|
||||
);
|
||||
)
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Confirm");
|
||||
button.onClick(async () => {
|
||||
new PasswordModal(this.app, this.plugin, newPassword).open();
|
||||
});
|
||||
});
|
||||
|
||||
containerEl.createEl("h2", { text: "Debug" });
|
||||
|
||||
const syncPlanDiv = containerEl.createEl("div");
|
||||
syncPlanDiv.createEl("p", {
|
||||
text: "Sync plans are created every time after you trigger sync and before the actual sync.",
|
||||
});
|
||||
syncPlanDiv.createEl("p", {
|
||||
text: "They are useful to know what would actually happen in those sync.",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("export sync plans")
|
||||
.setDesc("export sync plans")
|
||||
@@ -284,5 +395,25 @@ class SaveRemoteSettingTab extends PluginSettingTab {
|
||||
new Notice("sync plans history (in db) deleted");
|
||||
});
|
||||
});
|
||||
|
||||
const syncMappingDiv = containerEl.createEl("div");
|
||||
syncMappingDiv.createEl("p", {
|
||||
text: "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects.",
|
||||
});
|
||||
|
||||
syncMappingDiv.createEl("p", {
|
||||
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")
|
||||
.setDesc("delete sync mappings history in db")
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Delete Sync Mappings");
|
||||
button.onClick(async () => {
|
||||
await clearAllSyncMetaMapping(this.plugin.db);
|
||||
new Notice("sync mappings history (in local db) deleted");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
GetObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
HeadObjectCommand,
|
||||
HeadBucketCommand,
|
||||
ListObjectsV2CommandInput,
|
||||
ListObjectsV2CommandOutput,
|
||||
} from "@aws-sdk/client-s3";
|
||||
|
||||
import type { _Object } from "@aws-sdk/client-s3";
|
||||
@@ -129,17 +132,42 @@ export const listFromRemote = async (
|
||||
s3Config: S3Config,
|
||||
prefix?: string
|
||||
) => {
|
||||
if (prefix !== undefined) {
|
||||
return await s3Client.send(
|
||||
new ListObjectsV2Command({
|
||||
const confCmd = {
|
||||
Bucket: s3Config.s3BucketName,
|
||||
Prefix: prefix,
|
||||
})
|
||||
);
|
||||
} as ListObjectsV2CommandInput;
|
||||
if (prefix !== undefined) {
|
||||
confCmd.Prefix = prefix;
|
||||
}
|
||||
return await s3Client.send(
|
||||
new ListObjectsV2Command({ Bucket: s3Config.s3BucketName })
|
||||
);
|
||||
|
||||
const contents = [] as _Object[];
|
||||
|
||||
let isTruncated = true;
|
||||
let continuationToken = "";
|
||||
do {
|
||||
const rsp = await s3Client.send(new ListObjectsV2Command(confCmd));
|
||||
|
||||
if (rsp.$metadata.httpStatusCode !== 200) {
|
||||
throw Error("some thing bad while listing remote!");
|
||||
}
|
||||
contents.push(...rsp.Contents);
|
||||
|
||||
isTruncated = rsp.IsTruncated;
|
||||
confCmd.ContinuationToken = rsp.NextContinuationToken;
|
||||
if (
|
||||
isTruncated &&
|
||||
(continuationToken === undefined || continuationToken === "")
|
||||
) {
|
||||
throw Error("isTruncated is true but no continuationToken provided");
|
||||
}
|
||||
} while (isTruncated);
|
||||
|
||||
// ensemble fake rsp
|
||||
return {
|
||||
"$.metadata": {
|
||||
httpStatusCode: 200,
|
||||
},
|
||||
Contents: contents,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -266,3 +294,31 @@ export const deleteFromRemote = async (
|
||||
// pass
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the config of S3 by heading bucket
|
||||
* https://stackoverflow.com/questions/50842835
|
||||
* @param s3Client
|
||||
* @param s3Config
|
||||
* @returns
|
||||
*/
|
||||
export const checkS3Connectivity = async (
|
||||
s3Client: S3Client,
|
||||
s3Config: S3Config
|
||||
) => {
|
||||
try {
|
||||
const results = await s3Client.send(
|
||||
new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
|
||||
);
|
||||
if (
|
||||
results === undefined ||
|
||||
results.$metadata === undefined ||
|
||||
results.$metadata.httpStatusCode === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return results.$metadata.httpStatusCode === 200;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
+90
-9
@@ -17,13 +17,18 @@ import {
|
||||
downloadFromRemote,
|
||||
} from "./s3";
|
||||
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc";
|
||||
import { decryptBase32ToString, encryptStringToBase32 } from "./encrypt";
|
||||
import {
|
||||
decryptBase32ToString,
|
||||
encryptStringToBase32,
|
||||
MAGIC_ENCRYPTED_PREFIX_BASE32,
|
||||
} from "./encrypt";
|
||||
|
||||
export type SyncStatusType =
|
||||
| "idle"
|
||||
| "preparing"
|
||||
| "getting_remote_meta"
|
||||
| "getting_local_meta"
|
||||
| "checking_password"
|
||||
| "generating_plan"
|
||||
| "syncing"
|
||||
| "finish";
|
||||
@@ -61,6 +66,66 @@ export interface SyncPlanType {
|
||||
mixedStates: Record<string, FileOrFolderMixedState>;
|
||||
}
|
||||
|
||||
export interface PasswordCheckType {
|
||||
ok: boolean;
|
||||
reason:
|
||||
| "ok"
|
||||
| "empty_remote"
|
||||
| "remote_encrypted_local_no_password"
|
||||
| "password_matched"
|
||||
| "password_not_matched"
|
||||
| "remote_not_encrypted_local_has_password"
|
||||
| "no_password_both_sides";
|
||||
}
|
||||
|
||||
export const isPasswordOk = async (
|
||||
remote: S3ObjectType[],
|
||||
password: string = ""
|
||||
) => {
|
||||
if (remote === undefined || remote.length === 0) {
|
||||
// remote empty
|
||||
return {
|
||||
ok: true,
|
||||
reason: "empty_remote",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
const santyCheckKey = remote[0].Key;
|
||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
||||
// this is encrypted!
|
||||
// try to decrypt it using the provided password.
|
||||
if (password === "") {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "remote_encrypted_local_no_password",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
try {
|
||||
const res = await decryptBase32ToString(santyCheckKey, password);
|
||||
return {
|
||||
ok: true,
|
||||
reason: "password_matched",
|
||||
} as PasswordCheckType;
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "password_not_matched",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
} else {
|
||||
// it is not encrypted!
|
||||
if (password !== "") {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "remote_not_encrypted_local_has_password",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
reason: "no_password_both_sides",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
};
|
||||
|
||||
const ensembleMixedStates = async (
|
||||
remote: S3ObjectType[],
|
||||
local: TAbstractFile[],
|
||||
@@ -190,7 +255,8 @@ const ensembleMixedStates = async (
|
||||
|
||||
const getOperation = (
|
||||
origRecord: FileOrFolderMixedState,
|
||||
inplace: boolean = false
|
||||
inplace: boolean = false,
|
||||
password: string = ""
|
||||
) => {
|
||||
let r = origRecord;
|
||||
if (!inplace) {
|
||||
@@ -238,6 +304,7 @@ const getOperation = (
|
||||
r.mtime_remote !== undefined &&
|
||||
r.mtime_local !== undefined &&
|
||||
r.mtime_remote === r.mtime_local &&
|
||||
password === "" &&
|
||||
r.size_local === r.size_remote
|
||||
) {
|
||||
r.decision = "skip";
|
||||
@@ -248,17 +315,31 @@ const getOperation = (
|
||||
r.mtime_remote !== undefined &&
|
||||
r.mtime_local !== undefined &&
|
||||
r.mtime_remote === r.mtime_local &&
|
||||
password === "" &&
|
||||
r.size_local !== r.size_remote
|
||||
) {
|
||||
r.decision = "upload_clearhist";
|
||||
r.decision_branch = 4;
|
||||
} else if (
|
||||
r.exist_remote &&
|
||||
r.exist_local &&
|
||||
r.mtime_remote !== undefined &&
|
||||
r.mtime_local !== undefined &&
|
||||
r.mtime_remote === r.mtime_local &&
|
||||
password !== ""
|
||||
) {
|
||||
// if we have encryption,
|
||||
// the size is always unequal
|
||||
// only mtime(s) are reliable
|
||||
r.decision = "skip";
|
||||
r.decision_branch = 5;
|
||||
} else if (r.exist_remote && r.exist_local && r.mtime_local === undefined) {
|
||||
// this must be a folder!
|
||||
if (!r.key.endsWith("/")) {
|
||||
throw Error(`${r.key} is not a folder but lacks local mtime`);
|
||||
}
|
||||
r.decision = "skip";
|
||||
r.decision_branch = 5;
|
||||
r.decision_branch = 6;
|
||||
} else if (
|
||||
r.exist_remote &&
|
||||
!r.exist_local &&
|
||||
@@ -268,7 +349,7 @@ const getOperation = (
|
||||
r.mtime_remote >= r.delete_time_local
|
||||
) {
|
||||
r.decision = "download_clearhist";
|
||||
r.decision_branch = 6;
|
||||
r.decision_branch = 7;
|
||||
} else if (
|
||||
r.exist_remote &&
|
||||
!r.exist_local &&
|
||||
@@ -278,7 +359,7 @@ const getOperation = (
|
||||
r.mtime_remote < r.delete_time_local
|
||||
) {
|
||||
r.decision = "delremote_clearhist";
|
||||
r.decision_branch = 7;
|
||||
r.decision_branch = 8;
|
||||
} else if (
|
||||
r.exist_remote &&
|
||||
!r.exist_local &&
|
||||
@@ -287,10 +368,10 @@ const getOperation = (
|
||||
r.delete_time_local == undefined
|
||||
) {
|
||||
r.decision = "download";
|
||||
r.decision_branch = 8;
|
||||
r.decision_branch = 9;
|
||||
} else if (!r.exist_remote && r.exist_local && r.mtime_remote === undefined) {
|
||||
r.decision = "upload_clearhist";
|
||||
r.decision_branch = 9;
|
||||
r.decision_branch = 10;
|
||||
} else if (
|
||||
!r.exist_remote &&
|
||||
!r.exist_local &&
|
||||
@@ -298,7 +379,7 @@ const getOperation = (
|
||||
r.mtime_local === undefined
|
||||
) {
|
||||
r.decision = "clearhist";
|
||||
r.decision_branch = 10;
|
||||
r.decision_branch = 11;
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -319,7 +400,7 @@ export const getSyncPlan = async (
|
||||
password
|
||||
);
|
||||
for (const [key, val] of Object.entries(mixedStates)) {
|
||||
getOperation(val, true);
|
||||
getOperation(val, true, password);
|
||||
}
|
||||
const plan = {
|
||||
ts: Date.now(),
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
/* set the styles */
|
||||
|
||||
.password_second_confirm {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"0.0.8": "0.12.15"
|
||||
"0.0.11": "0.12.15"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user