Compare commits

...
13 Commits
Author SHA1 Message Date
fyears fde4c9a294 0.0.11 2021-11-08 00:40:45 +08:00
fyears 7fc38a942a check password 2021-11-08 00:39:17 +08:00
fyears cc0c4fe224 second confirm for password changes 2021-11-08 00:17:42 +08:00
fyears 2ab5640c12 list all objects 2021-11-07 17:15:06 +08:00
fyears b7a0376a0f allow clearing sync mappings 2021-11-07 16:02:03 +08:00
fyears d7d1c47e2a long desc for debug 2021-11-07 15:44:14 +08:00
fyears a65f5eb004 head bucket 2021-11-07 15:43:57 +08:00
fyears 2ce34379ad format doc 2021-11-07 15:27:41 +08:00
fyears 40d2ee95b7 0.0.10 2021-11-07 15:01:12 +08:00
fyears 5632e28331 make decision works 2021-11-07 15:00:22 +08:00
fyears 2b53a1bf5e to 0.0.9 2021-11-07 14:54:25 +08:00
fyears ccd9e3116c add cross-env 2021-11-07 14:51:00 +08:00
fyears e8d6a50793 update algo 2021-11-07 14:24:24 +08:00
10 changed files with 328 additions and 47 deletions
+9 -9
View File
@@ -15,15 +15,15 @@ Assuming all sources are reliable.
We list all combinations mutually exclusive and collectively exhaustive. We list all combinations mutually exclusive and collectively exhaustive.
| ID | Remote Files | Local files | Local delete rename history | Extra | Decision | | 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 | | 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 | | 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) | | 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 && size_remote !== size_local | upload local file, clear local history if exists (we always prefer local to remote) | | 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 | 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? | | 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 | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists | | 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 | delete remote file, clear local history | | 7 | exist | not exist | exist | mtime_remote >= delete_time_local | download remote file, create folder if not exists |
| 8 | exist | not exist | not exist | | 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 | not exist | exist | ignore | If local is a single file. | upload local file, create remote folder if not exists, clear local history if exists | | 9 | exist | not exist | not exist | | download remote file, create folder if not 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 | | 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 | | 11 | not exist | not exist | ignore | | clear local history if exists |
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "obsdian-save-remote", "id": "obsdian-save-remote",
"name": "Save remote", "name": "Save remote",
"version": "0.0.8", "version": "0.0.11",
"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",
+3 -2
View File
@@ -1,13 +1,13 @@
{ {
"name": "obsidian-save-remote", "name": "obsidian-save-remote",
"version": "0.0.8", "version": "0.0.11",
"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",
"build": "webpack --mode production", "build": "webpack --mode production",
"format": "npx prettier --write .", "format": "npx prettier --write .",
"clean": "npx rimraf main.js", "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", "source": "main.ts",
"keywords": [], "keywords": [],
@@ -19,6 +19,7 @@
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
"chai": "^4.3.4", "chai": "^4.3.4",
"cross-env": "^7.0.3",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"terser-webpack-plugin": "^5.2.4", "terser-webpack-plugin": "^5.2.4",
+3
View File
@@ -8,6 +8,9 @@ import {
const DEFAULT_ITER = 10000; const DEFAULT_ITER = 10000;
// base32.stringify(Buffer.from('Salted__'))
export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV";
const getKeyIVFromPassword = async ( const getKeyIVFromPassword = async (
salt: Uint8Array, salt: Uint8Array,
password: string, password: string,
+5
View File
@@ -276,6 +276,11 @@ export const getSyncMetaMappingByRemoteKeyS3 = async (
throw Error("something bad in sync meta mapping!"); 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 ( export const insertSyncPlanRecord = async (
db: lf.DatabaseConnection, db: lf.DatabaseConnection,
syncPlan: SyncPlanType syncPlan: SyncPlanType
+143 -12
View File
@@ -11,7 +11,11 @@ import {
TFolder, TFolder,
} from "obsidian"; } from "obsidian";
import * as CodeMirror from "codemirror"; import * as CodeMirror from "codemirror";
import { clearAllSyncPlanRecords, DatabaseConnection } from "./localdb"; import {
clearAllSyncPlanRecords,
clearAllSyncMetaMapping,
DatabaseConnection,
} from "./localdb";
import { import {
prepareDBs, prepareDBs,
destroyDBs, destroyDBs,
@@ -22,9 +26,15 @@ import {
insertSyncPlanRecord, insertSyncPlanRecord,
} from "./localdb"; } from "./localdb";
import type { SyncStatusType } from "./sync"; import type { SyncStatusType, PasswordCheckType } from "./sync";
import { getSyncPlan, doActualSync } from "./sync"; import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
import { DEFAULT_S3_CONFIG, getS3Client, listFromRemote, S3Config } from "./s3"; import {
DEFAULT_S3_CONFIG,
getS3Client,
listFromRemote,
S3Config,
checkS3Connectivity,
} from "./s3";
import { exportSyncPlansToFiles } from "./debugMode"; import { exportSyncPlansToFiles } from "./debugMode";
interface SaveRemotePluginSettings { interface SaveRemotePluginSettings {
@@ -71,6 +81,7 @@ export default class SaveRemotePlugin extends Plugin {
} }
try { try {
//console.log(`huh ${this.settings.password}`)
new Notice("1/6 Save Remote Sync Preparing"); new Notice("1/6 Save Remote Sync Preparing");
this.syncStatus = "preparing"; this.syncStatus = "preparing";
@@ -87,7 +98,17 @@ export default class SaveRemotePlugin extends Plugin {
// console.log(local); // console.log(local);
// console.log(localHistory); // 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"; this.syncStatus = "generating_plan";
const syncPlan = await getSyncPlan( const syncPlan = await getSyncPlan(
remoteRsp.Contents, remoteRsp.Contents,
@@ -102,7 +123,7 @@ export default class SaveRemotePlugin extends Plugin {
// The operations above are read only and kind of safe. // The operations above are read only and kind of safe.
// The operations below begins to write or delete (!!!) something. // 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"; this.syncStatus = "syncing";
await doActualSync( await doActualSync(
@@ -114,7 +135,7 @@ export default class SaveRemotePlugin extends Plugin {
this.settings.password this.settings.password
); );
new Notice("6/6 Save Remote finish!"); new Notice("7/7 Save Remote finish!");
this.syncStatus = "finish"; this.syncStatus = "finish";
this.syncStatus = "idle"; this.syncStatus = "idle";
} catch (error) { } catch (error) {
@@ -122,7 +143,7 @@ export default class SaveRemotePlugin extends Plugin {
console.log(msg); console.log(msg);
console.log(error); console.log(error);
new Notice(msg); new Notice(msg);
new Notice(error); new Notice(error.message);
this.syncStatus = "idle"; 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 { class SaveRemoteSettingTab extends PluginSettingTab {
plugin: SaveRemotePlugin; 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" }); containerEl.createEl("h2", { text: "General" });
let newPassword = `${this.plugin.settings.password}`;
new Setting(containerEl) new Setting(containerEl)
.setName("encryption password") .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) => .addText((text) =>
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(`${this.plugin.settings.password}`) .setValue(`${this.plugin.settings.password}`)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.password = value.trim(); newPassword = value.trim();
await this.plugin.saveSettings();
}) })
); )
.addButton(async (button) => {
button.setButtonText("Confirm");
button.onClick(async () => {
new PasswordModal(this.app, this.plugin, newPassword).open();
});
});
containerEl.createEl("h2", { text: "Debug" }); 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) new Setting(containerEl)
.setName("export sync plans") .setName("export sync plans")
.setDesc("export sync plans") .setDesc("export sync plans")
@@ -284,5 +395,25 @@ class SaveRemoteSettingTab extends PluginSettingTab {
new Notice("sync plans history (in db) deleted"); 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");
});
});
} }
} }
+65 -9
View File
@@ -10,6 +10,9 @@ import {
GetObjectCommand, GetObjectCommand,
DeleteObjectCommand, DeleteObjectCommand,
HeadObjectCommand, HeadObjectCommand,
HeadBucketCommand,
ListObjectsV2CommandInput,
ListObjectsV2CommandOutput,
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import type { _Object } from "@aws-sdk/client-s3"; import type { _Object } from "@aws-sdk/client-s3";
@@ -129,17 +132,42 @@ export const listFromRemote = async (
s3Config: S3Config, s3Config: S3Config,
prefix?: string prefix?: string
) => { ) => {
if (prefix !== undefined) { const confCmd = {
return await s3Client.send(
new ListObjectsV2Command({
Bucket: s3Config.s3BucketName, 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 // 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
View File
@@ -17,13 +17,18 @@ import {
downloadFromRemote, downloadFromRemote,
} from "./s3"; } from "./s3";
import { mkdirpInVault, SUPPORTED_SERVICES_TYPE, isHiddenPath } from "./misc"; 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 = export type SyncStatusType =
| "idle" | "idle"
| "preparing" | "preparing"
| "getting_remote_meta" | "getting_remote_meta"
| "getting_local_meta" | "getting_local_meta"
| "checking_password"
| "generating_plan" | "generating_plan"
| "syncing" | "syncing"
| "finish"; | "finish";
@@ -61,6 +66,66 @@ export interface SyncPlanType {
mixedStates: Record<string, FileOrFolderMixedState>; 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 ( const ensembleMixedStates = async (
remote: S3ObjectType[], remote: S3ObjectType[],
local: TAbstractFile[], local: TAbstractFile[],
@@ -190,7 +255,8 @@ const ensembleMixedStates = async (
const getOperation = ( const getOperation = (
origRecord: FileOrFolderMixedState, origRecord: FileOrFolderMixedState,
inplace: boolean = false inplace: boolean = false,
password: string = ""
) => { ) => {
let r = origRecord; let r = origRecord;
if (!inplace) { if (!inplace) {
@@ -238,6 +304,7 @@ const getOperation = (
r.mtime_remote !== undefined && r.mtime_remote !== undefined &&
r.mtime_local !== undefined && r.mtime_local !== undefined &&
r.mtime_remote === r.mtime_local && r.mtime_remote === r.mtime_local &&
password === "" &&
r.size_local === r.size_remote r.size_local === r.size_remote
) { ) {
r.decision = "skip"; r.decision = "skip";
@@ -248,17 +315,31 @@ const getOperation = (
r.mtime_remote !== undefined && r.mtime_remote !== undefined &&
r.mtime_local !== undefined && r.mtime_local !== undefined &&
r.mtime_remote === r.mtime_local && r.mtime_remote === r.mtime_local &&
password === "" &&
r.size_local !== r.size_remote r.size_local !== r.size_remote
) { ) {
r.decision = "upload_clearhist"; r.decision = "upload_clearhist";
r.decision_branch = 4; 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) { } else if (r.exist_remote && r.exist_local && r.mtime_local === undefined) {
// this must be a folder! // this must be a folder!
if (!r.key.endsWith("/")) { if (!r.key.endsWith("/")) {
throw Error(`${r.key} is not a folder but lacks local mtime`); throw Error(`${r.key} is not a folder but lacks local mtime`);
} }
r.decision = "skip"; r.decision = "skip";
r.decision_branch = 5; r.decision_branch = 6;
} else if ( } else if (
r.exist_remote && r.exist_remote &&
!r.exist_local && !r.exist_local &&
@@ -268,7 +349,7 @@ const getOperation = (
r.mtime_remote >= r.delete_time_local r.mtime_remote >= r.delete_time_local
) { ) {
r.decision = "download_clearhist"; r.decision = "download_clearhist";
r.decision_branch = 6; r.decision_branch = 7;
} else if ( } else if (
r.exist_remote && r.exist_remote &&
!r.exist_local && !r.exist_local &&
@@ -278,7 +359,7 @@ const getOperation = (
r.mtime_remote < r.delete_time_local r.mtime_remote < r.delete_time_local
) { ) {
r.decision = "delremote_clearhist"; r.decision = "delremote_clearhist";
r.decision_branch = 7; r.decision_branch = 8;
} else if ( } else if (
r.exist_remote && r.exist_remote &&
!r.exist_local && !r.exist_local &&
@@ -287,10 +368,10 @@ const getOperation = (
r.delete_time_local == undefined r.delete_time_local == undefined
) { ) {
r.decision = "download"; r.decision = "download";
r.decision_branch = 8; r.decision_branch = 9;
} else if (!r.exist_remote && r.exist_local && r.mtime_remote === undefined) { } else if (!r.exist_remote && r.exist_local && r.mtime_remote === undefined) {
r.decision = "upload_clearhist"; r.decision = "upload_clearhist";
r.decision_branch = 9; r.decision_branch = 10;
} else if ( } else if (
!r.exist_remote && !r.exist_remote &&
!r.exist_local && !r.exist_local &&
@@ -298,7 +379,7 @@ const getOperation = (
r.mtime_local === undefined r.mtime_local === undefined
) { ) {
r.decision = "clearhist"; r.decision = "clearhist";
r.decision_branch = 10; r.decision_branch = 11;
} }
return r; return r;
@@ -319,7 +400,7 @@ export const getSyncPlan = async (
password password
); );
for (const [key, val] of Object.entries(mixedStates)) { for (const [key, val] of Object.entries(mixedStates)) {
getOperation(val, true); getOperation(val, true, password);
} }
const plan = { const plan = {
ts: Date.now(), ts: Date.now(),
+4
View File
@@ -1 +1,5 @@
/* set the styles */ /* set the styles */
.password_second_confirm {
font-weight: bold;
}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"0.0.8": "0.12.15" "0.0.11": "0.12.15"
} }