Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
996074e431 | ||
|
|
275224cf73 | ||
|
|
575a27db26 | ||
|
|
76127dd131 | ||
|
|
e2e6f6a12c | ||
|
|
4f3e8803da | ||
|
|
63a66d69f9 | ||
|
|
79fb1d90d0 | ||
|
|
ac4eff376d | ||
|
|
5512978dee | ||
|
|
2bac0c32dc | ||
|
|
1cd02913bb | ||
|
|
51f6d600a6 | ||
|
|
a4905445c7 | ||
|
|
12d78444b3 | ||
|
|
dffada894f | ||
|
|
50ae224769 | ||
|
|
5add7ad645 |
@@ -30,6 +30,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
submodules: recursive
|
||||
- name: Checkout LFS
|
||||
run: git lfs checkout
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "src/langs"]
|
||||
path = src/langs
|
||||
url = https://github.com/remotely-save/langs.git
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.10",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.10",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
@@ -26,11 +26,11 @@
|
||||
"@microsoft/microsoft-graph-types": "^2.11.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/feather-icons": "^4.7.0",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/qrcode": "^1.4.1",
|
||||
"builtin-modules": "^3.2.0",
|
||||
@@ -68,12 +68,13 @@
|
||||
"codemirror": "^5.63.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"dropbox": "^10.22.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"lucide": "^0.17.12",
|
||||
"mime-types": "^2.1.33",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^3.1.30",
|
||||
"obsidian": "^0.13.26",
|
||||
"p-queue": "^7.2.0",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* To avoid circular dependency.
|
||||
*/
|
||||
|
||||
import type { LangType, LangTypeAndAuto } from "./i18n";
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive";
|
||||
|
||||
export interface S3Config {
|
||||
@@ -13,6 +15,7 @@ export interface S3Config {
|
||||
s3BucketName: string;
|
||||
bypassCorsLocally?: boolean;
|
||||
partsConcurrency?: number;
|
||||
forcePathStyle?: boolean;
|
||||
}
|
||||
|
||||
export interface DropboxConfig {
|
||||
@@ -70,6 +73,7 @@ export interface RemotelySavePluginSettings {
|
||||
concurrency?: number;
|
||||
syncConfigDir?: boolean;
|
||||
syncUnderscoreItems?: boolean;
|
||||
lang?: LangTypeAndAuto;
|
||||
}
|
||||
|
||||
export interface RemoteItem {
|
||||
|
||||
@@ -165,3 +165,23 @@ export const decryptBase64urlToString = async (
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const getSizeFromOrigToEnc = (x: number) => {
|
||||
if (x < 0 || !Number.isInteger(x)) {
|
||||
throw Error(`x=${x} is not a valid size`);
|
||||
}
|
||||
return (Math.floor(x / 16) + 1) * 16 + 16;
|
||||
};
|
||||
|
||||
export const getSizeFromEncToOrig = (x: number) => {
|
||||
if (x < 32 || !Number.isInteger(x)) {
|
||||
throw Error(`${x} is not a valid size`);
|
||||
}
|
||||
if (x % 16 !== 0) {
|
||||
throw Error(`${x} is not a valid encrypted file size`);
|
||||
}
|
||||
return {
|
||||
minSize: ((x - 16) / 16 - 1) * 16,
|
||||
maxSize: ((x - 16) / 16 - 1) * 16 + 15,
|
||||
};
|
||||
};
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import Mustache from "mustache";
|
||||
import { moment } from "obsidian";
|
||||
|
||||
import { LANGS } from "./langs";
|
||||
|
||||
export type LangType = keyof typeof LANGS;
|
||||
export type LangTypeAndAuto = LangType | "auto";
|
||||
export type TransItemType = keyof typeof LANGS["en"];
|
||||
|
||||
export class I18n {
|
||||
lang: LangTypeAndAuto;
|
||||
readonly saveSettingFunc: (tolang: LangTypeAndAuto) => Promise<void>;
|
||||
constructor(
|
||||
lang: LangTypeAndAuto,
|
||||
saveSettingFunc: (tolang: LangTypeAndAuto) => Promise<void>
|
||||
) {
|
||||
this.lang = lang;
|
||||
this.saveSettingFunc = saveSettingFunc;
|
||||
}
|
||||
async changeTo(anotherLang: LangTypeAndAuto) {
|
||||
this.lang = anotherLang;
|
||||
await this.saveSettingFunc(anotherLang);
|
||||
}
|
||||
|
||||
_get(key: TransItemType) {
|
||||
let realLang = this.lang;
|
||||
if (this.lang === "auto" && moment.locale().replace("-", "_") in LANGS) {
|
||||
realLang = moment.locale().replace("-", "_") as LangType;
|
||||
} else {
|
||||
realLang = "en";
|
||||
}
|
||||
|
||||
const res: string =
|
||||
(LANGS[realLang] as typeof LANGS["en"])[key] || LANGS["en"][key] || key;
|
||||
return res;
|
||||
}
|
||||
|
||||
t(key: TransItemType, vars?: Record<string, string>) {
|
||||
if (vars === undefined) {
|
||||
return this._get(key);
|
||||
}
|
||||
return Mustache.render(this._get(key), vars);
|
||||
}
|
||||
}
|
||||
Submodule
+1
Submodule src/langs added at 28730585f8
+133
-60
@@ -1,7 +1,7 @@
|
||||
import { Modal, Notice, Plugin, Setting, addIcon, setIcon } from "obsidian";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { nanoid } from "nanoid";
|
||||
import feather from "feather-icons";
|
||||
import { createElement, RotateCcw, RefreshCcw } from "lucide";
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
import {
|
||||
COMMAND_CALLBACK,
|
||||
@@ -38,6 +38,8 @@ import { fetchMetadataFile, parseRemoteItems, SyncStatusType } from "./sync";
|
||||
import { doActualSync, getSyncPlan, isPasswordOk } from "./sync";
|
||||
import { messyConfigToNormal, normalConfigToMessy } from "./configPersist";
|
||||
import { ObsConfigDirFileType, listFilesInObsFolder } from "./obsFolderLister";
|
||||
import { I18n } from "./i18n";
|
||||
import type { LangType, LangTypeAndAuto, TransItemType } from "./i18n";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
import { DeletionOnRemote, MetadataOnRemote } from "./metadataOnRemote";
|
||||
@@ -59,6 +61,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
concurrency: 5,
|
||||
syncConfigDir: false,
|
||||
syncUnderscoreItems: false,
|
||||
lang: "auto",
|
||||
};
|
||||
|
||||
interface OAuth2Info {
|
||||
@@ -73,14 +76,13 @@ type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
||||
|
||||
const iconNameSyncWait = `remotely-save-sync-wait`;
|
||||
const iconNameSyncRunning = `remotely-save-sync-running`;
|
||||
const iconSvgSyncWait = feather.icons["rotate-ccw"].toSvg({
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
const iconSvgSyncRunning = feather.icons["refresh-ccw"].toSvg({
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
|
||||
const iconSvgSyncWait = createElement(RotateCcw);
|
||||
iconSvgSyncWait.setAttribute("width", "100");
|
||||
iconSvgSyncWait.setAttribute("height", "100");
|
||||
const iconSvgSyncRunning = createElement(RefreshCcw);
|
||||
iconSvgSyncRunning.setAttribute("width", "100");
|
||||
iconSvgSyncRunning.setAttribute("height", "100");
|
||||
|
||||
export default class RemotelySavePlugin extends Plugin {
|
||||
settings: RemotelySavePluginSettings;
|
||||
@@ -92,18 +94,28 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
currSyncMsg?: string;
|
||||
syncRibbon?: HTMLElement;
|
||||
autoRunIntervalID?: number;
|
||||
i18n: I18n;
|
||||
|
||||
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
||||
const getNotice = (x: string) => {
|
||||
const t = (x: TransItemType, vars?: any) => {
|
||||
return this.i18n.t(x, vars);
|
||||
};
|
||||
|
||||
const getNotice = (x: string, timeout?: number) => {
|
||||
// only show notices in manual mode
|
||||
// no notice in auto mode
|
||||
if (triggerSource === "manual" || triggerSource === "dry") {
|
||||
new Notice(x);
|
||||
new Notice(x, timeout);
|
||||
}
|
||||
};
|
||||
if (this.syncStatus !== "idle") {
|
||||
// here the notice is shown regardless of triggerSource
|
||||
new Notice(`Remotely Save already running in stage ${this.syncStatus}!`);
|
||||
new Notice(
|
||||
t("syncrun_alreadyrunning", {
|
||||
pluginName: this.manifest.name,
|
||||
syncStatus: this.syncStatus,
|
||||
})
|
||||
);
|
||||
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
||||
new Notice(this.currSyncMsg);
|
||||
}
|
||||
@@ -126,7 +138,10 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
setIcon(this.syncRibbon, iconNameSyncRunning);
|
||||
this.syncRibbon.setAttribute(
|
||||
"aria-label",
|
||||
`${this.manifest.name}: ${triggerSource} syncing`
|
||||
t("syncrun_syncingribbon", {
|
||||
pluginName: this.manifest.name,
|
||||
triggerSource: triggerSource,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,17 +149,26 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
if (triggerSource === "dry") {
|
||||
getNotice(
|
||||
`0/${MAX_STEPS} Remotely Save running in dry mode, not actual file changes would happen.`
|
||||
t("syncrun_step0", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
//log.info(`huh ${this.settings.password}`)
|
||||
getNotice(
|
||||
`1/${MAX_STEPS} Remotely Save Sync Preparing (${this.settings.serviceType})`
|
||||
t("syncrun_step1", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
serviceType: this.settings.serviceType,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "preparing";
|
||||
|
||||
getNotice(`2/${MAX_STEPS} Starting to fetch remote meta data.`);
|
||||
getNotice(
|
||||
t("syncrun_step2", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "getting_remote_files_list";
|
||||
const self = this;
|
||||
const client = new RemoteClient(
|
||||
@@ -159,18 +183,26 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
const remoteRsp = await client.listFromRemote();
|
||||
log.debug(remoteRsp);
|
||||
|
||||
getNotice(`3/${MAX_STEPS} Checking password correct or not.`);
|
||||
getNotice(
|
||||
t("syncrun_step3", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "checking_password";
|
||||
const passwordCheckResult = await isPasswordOk(
|
||||
remoteRsp.Contents,
|
||||
this.settings.password
|
||||
);
|
||||
if (!passwordCheckResult.ok) {
|
||||
getNotice("something goes wrong while checking password");
|
||||
getNotice(t("syncrun_passworderr"));
|
||||
throw Error(passwordCheckResult.reason);
|
||||
}
|
||||
|
||||
getNotice(`4/${MAX_STEPS} Trying to fetch extra meta data from remote.`);
|
||||
getNotice(
|
||||
t("syncrun_step4", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "getting_remote_extra_meta";
|
||||
const { remoteStates, metadataFile } = await parseRemoteItems(
|
||||
remoteRsp.Contents,
|
||||
@@ -186,7 +218,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.password
|
||||
);
|
||||
|
||||
getNotice(`5/${MAX_STEPS} Starting to fetch local meta data.`);
|
||||
getNotice(
|
||||
t("syncrun_step5", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "getting_local_meta";
|
||||
const local = this.app.vault.getAllLoadedFiles();
|
||||
const localHistory = await loadDeleteRenameHistoryTableByVault(
|
||||
@@ -204,7 +240,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
// log.info(local);
|
||||
// log.info(localHistory);
|
||||
|
||||
getNotice(`6/${MAX_STEPS} Starting to generate sync plan.`);
|
||||
getNotice(
|
||||
t("syncrun_step6", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "generating_plan";
|
||||
const { plan, sortedKeys, deletions } = await getSyncPlan(
|
||||
remoteStates,
|
||||
@@ -232,7 +272,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
// The operations below begins to write or delete (!!!) something.
|
||||
|
||||
if (triggerSource !== "dry") {
|
||||
getNotice(`7/${MAX_STEPS} Remotely Save Sync data exchanging!`);
|
||||
getNotice(
|
||||
t("syncrun_step7", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
|
||||
this.syncStatus = "syncing";
|
||||
await doActualSync(
|
||||
@@ -254,11 +298,17 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
} else {
|
||||
this.syncStatus = "syncing";
|
||||
getNotice(
|
||||
`7/${MAX_STEPS} Remotely Save real sync is skipped in dry run mode.`
|
||||
t("syncrun_step7skip", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getNotice(`8/${MAX_STEPS} Remotely Save finish!`);
|
||||
getNotice(
|
||||
t("syncrun_step8", {
|
||||
maxSteps: `${MAX_STEPS}`,
|
||||
})
|
||||
);
|
||||
this.syncStatus = "finish";
|
||||
this.syncStatus = "idle";
|
||||
|
||||
@@ -273,15 +323,16 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = `${
|
||||
this.manifest.id
|
||||
}-${Date.now()}: abort sync, triggerSource=${triggerSource}, error while ${
|
||||
this.syncStatus
|
||||
}`;
|
||||
const msg = t("syncrun_abort", {
|
||||
manifestID: this.manifest.id,
|
||||
theDate: `${Date.now()}`,
|
||||
triggerSource: triggerSource,
|
||||
syncStatus: this.syncStatus,
|
||||
});
|
||||
log.info(msg);
|
||||
log.info(error);
|
||||
getNotice(msg);
|
||||
getNotice(error.message);
|
||||
getNotice(msg, 10 * 1000);
|
||||
getNotice(error.message, 10 * 1000);
|
||||
this.syncStatus = "idle";
|
||||
if (this.syncRibbon !== undefined) {
|
||||
setIcon(this.syncRibbon, iconNameSyncWait);
|
||||
@@ -293,8 +344,8 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
async onload() {
|
||||
log.info(`loading plugin ${this.manifest.id}`);
|
||||
|
||||
addIcon(iconNameSyncWait, iconSvgSyncWait);
|
||||
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
|
||||
addIcon(iconNameSyncWait, iconSvgSyncWait.outerHTML);
|
||||
addIcon(iconNameSyncRunning, iconSvgSyncRunning.outerHTML);
|
||||
|
||||
this.oauth2Info = {
|
||||
verifier: "",
|
||||
@@ -308,6 +359,15 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
// lang should be load early, but after settings
|
||||
this.i18n = new I18n(this.settings.lang, async (lang: LangTypeAndAuto) => {
|
||||
this.settings.lang = lang;
|
||||
await this.saveSettings();
|
||||
});
|
||||
const t = (x: TransItemType, vars?: any) => {
|
||||
return this.i18n.t(x, vars);
|
||||
};
|
||||
|
||||
if (this.settings.currLogLevel !== undefined) {
|
||||
log.setLevel(this.settings.currLogLevel as any);
|
||||
}
|
||||
@@ -353,7 +413,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings = Object.assign({}, this.settings, copied);
|
||||
this.saveSettings();
|
||||
new Notice(
|
||||
`New not-oauth2 settings for ${this.manifest.name} saved. Reopen the plugin Settings to the effect.`
|
||||
t("protocol_saveqr", {
|
||||
manifestName: this.manifest.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -362,9 +424,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
COMMAND_CALLBACK,
|
||||
async (inputParams) => {
|
||||
new Notice(
|
||||
`Your uri call a callback that's not supported yet: ${JSON.stringify(
|
||||
inputParams
|
||||
)}`
|
||||
t("protocol_callbacknotsupported", {
|
||||
params: JSON.stringify(inputParams),
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -375,11 +437,13 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
if (inputParams.code !== undefined) {
|
||||
if (this.oauth2Info.helperModal !== undefined) {
|
||||
this.oauth2Info.helperModal.contentEl.empty();
|
||||
|
||||
t("protocol_dropbox_connecting")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
||||
text: "Connecting to Dropbox...",
|
||||
text: val,
|
||||
});
|
||||
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
||||
text: "Please DO NOT close this modal.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -410,7 +474,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.dropbox.username = username;
|
||||
await this.saveSettings();
|
||||
|
||||
new Notice(`Good! We've connected to Dropbox as user ${username}!`);
|
||||
new Notice(
|
||||
t("protocol_dropbox_connect_succ", {
|
||||
username: username,
|
||||
})
|
||||
);
|
||||
|
||||
this.oauth2Info.verifier = ""; // reset it
|
||||
this.oauth2Info.helperModal?.close(); // close it
|
||||
@@ -423,7 +491,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.oauth2Info.authDiv = undefined;
|
||||
|
||||
this.oauth2Info.revokeAuthSetting?.setDesc(
|
||||
`You've connected as user ${this.settings.dropbox.username}. If you want to disconnect, click this button.`
|
||||
t("protocol_dropbox_connect_succ_revoke", {
|
||||
username: this.settings.dropbox.username,
|
||||
})
|
||||
);
|
||||
this.oauth2Info.revokeAuthSetting = undefined;
|
||||
this.oauth2Info.revokeDiv?.toggleClass(
|
||||
@@ -432,13 +502,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
);
|
||||
this.oauth2Info.revokeDiv = undefined;
|
||||
} else {
|
||||
new Notice(
|
||||
"Something went wrong from response from Dropbox. Maybe you rejected the auth?"
|
||||
);
|
||||
new Notice(t("protocol_dropbox_connect_fail"));
|
||||
throw Error(
|
||||
`do not know how to deal with the callback: ${JSON.stringify(
|
||||
inputParams
|
||||
)}`
|
||||
t("protocol_dropbox_connect_unknown", {
|
||||
params: JSON.stringify(inputParams),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -450,11 +518,13 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
if (inputParams.code !== undefined) {
|
||||
if (this.oauth2Info.helperModal !== undefined) {
|
||||
this.oauth2Info.helperModal.contentEl.empty();
|
||||
|
||||
t("protocol_onedrive_connecting")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
||||
text: "Connecting to Onedrive...",
|
||||
text: val,
|
||||
});
|
||||
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
||||
text: "Please DO NOT close this modal.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -499,7 +569,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.oauth2Info.authDiv = undefined;
|
||||
|
||||
this.oauth2Info.revokeAuthSetting?.setDesc(
|
||||
`You've connected as user ${this.settings.onedrive.username}. If you want to disconnect, click this button.`
|
||||
t("protocol_onedrive_connect_succ_revoke", {
|
||||
username: this.settings.onedrive.username,
|
||||
})
|
||||
);
|
||||
this.oauth2Info.revokeAuthSetting = undefined;
|
||||
this.oauth2Info.revokeDiv?.toggleClass(
|
||||
@@ -508,13 +580,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
);
|
||||
this.oauth2Info.revokeDiv = undefined;
|
||||
} else {
|
||||
new Notice(
|
||||
"Something went wrong from response from OneDrive. Maybe you rejected the auth?"
|
||||
);
|
||||
new Notice(t("protocol_onedrive_connect_fail"));
|
||||
throw Error(
|
||||
`do not know how to deal with the callback: ${JSON.stringify(
|
||||
inputParams
|
||||
)}`
|
||||
t("protocol_onedrive_connect_unknown", {
|
||||
params: JSON.stringify(inputParams),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -528,7 +598,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
this.addCommand({
|
||||
id: "start-sync",
|
||||
name: "start sync",
|
||||
name: t("command_startsync"),
|
||||
icon: iconNameSyncWait,
|
||||
callback: async () => {
|
||||
this.syncRun("manual");
|
||||
@@ -537,7 +607,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
this.addCommand({
|
||||
id: "start-sync-dry-run",
|
||||
name: "start sync (dry run only)",
|
||||
name: t("command_drynrun"),
|
||||
icon: iconNameSyncWait,
|
||||
callback: async () => {
|
||||
this.syncRun("dry");
|
||||
@@ -588,6 +658,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
if (this.settings.s3.partsConcurrency === undefined) {
|
||||
this.settings.s3.partsConcurrency = 20;
|
||||
}
|
||||
if (this.settings.s3.forcePathStyle === undefined) {
|
||||
this.settings.s3.forcePathStyle = false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
||||
+12
-5
@@ -271,15 +271,22 @@ export class RemoteClient {
|
||||
}
|
||||
};
|
||||
|
||||
checkConnectivity = async () => {
|
||||
checkConnectivity = async (callbackFunc?: any) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.checkConnectivity(this.s3Client, this.s3Config);
|
||||
return await s3.checkConnectivity(
|
||||
this.s3Client,
|
||||
this.s3Config,
|
||||
callbackFunc
|
||||
);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.checkConnectivity(this.webdavClient);
|
||||
return await webdav.checkConnectivity(this.webdavClient, callbackFunc);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.checkConnectivity(this.dropboxClient);
|
||||
return await dropbox.checkConnectivity(this.dropboxClient, callbackFunc);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.checkConnectivity(this.onedriveClient);
|
||||
return await onedrive.checkConnectivity(
|
||||
this.onedriveClient,
|
||||
callbackFunc
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
|
||||
@@ -636,7 +636,10 @@ export const deleteFromRemote = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedDropboxClient) => {
|
||||
export const checkConnectivity = async (
|
||||
client: WrappedDropboxClient,
|
||||
callbackFunc?: any
|
||||
) => {
|
||||
try {
|
||||
const results = await getRemoteMeta(client, "/");
|
||||
if (results === undefined) {
|
||||
@@ -644,8 +647,10 @@ export const checkConnectivity = async (client: WrappedDropboxClient) => {
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("dropbox connectivity error:");
|
||||
console.error(err);
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -866,11 +866,18 @@ export const deleteFromRemote = async (
|
||||
await client.deleteJson(remoteFileName);
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedOnedriveClient) => {
|
||||
export const checkConnectivity = async (
|
||||
client: WrappedOnedriveClient,
|
||||
callbackFunc?: any
|
||||
) => {
|
||||
try {
|
||||
const k = await getUserDisplayName(client);
|
||||
return k !== "<unknown display name>";
|
||||
} catch (err) {
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
+14
-1
@@ -155,6 +155,7 @@ export const DEFAULT_S3_CONFIG = {
|
||||
s3BucketName: "",
|
||||
bypassCorsLocally: true,
|
||||
partsConcurrency: 20,
|
||||
forcePathStyle: false,
|
||||
};
|
||||
|
||||
export type S3ObjectType = _Object;
|
||||
@@ -192,6 +193,7 @@ export const getS3Client = (s3Config: S3Config) => {
|
||||
const s3Client = new S3Client({
|
||||
region: s3Config.s3Region,
|
||||
endpoint: endpoint,
|
||||
forcePathStyle: s3Config.forcePathStyle,
|
||||
credentials: {
|
||||
accessKeyId: s3Config.s3AccessKeyID,
|
||||
secretAccessKey: s3Config.s3SecretAccessKey,
|
||||
@@ -203,6 +205,7 @@ export const getS3Client = (s3Config: S3Config) => {
|
||||
const s3Client = new S3Client({
|
||||
region: s3Config.s3Region,
|
||||
endpoint: endpoint,
|
||||
forcePathStyle: s3Config.forcePathStyle,
|
||||
credentials: {
|
||||
accessKeyId: s3Config.s3AccessKeyID,
|
||||
secretAccessKey: s3Config.s3SecretAccessKey,
|
||||
@@ -495,7 +498,8 @@ export const deleteFromRemote = async (
|
||||
*/
|
||||
export const checkConnectivity = async (
|
||||
s3Client: S3Client,
|
||||
s3Config: S3Config
|
||||
s3Config: S3Config,
|
||||
callbackFunc?: any
|
||||
) => {
|
||||
try {
|
||||
const results = await s3Client.send(
|
||||
@@ -506,10 +510,19 @@ export const checkConnectivity = async (
|
||||
results.$metadata === undefined ||
|
||||
results.$metadata.httpStatusCode === undefined
|
||||
) {
|
||||
const err = "results or $metadata or httStatusCode is undefined";
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return results.$metadata.httpStatusCode === 200;
|
||||
} catch (err) {
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
+27
-2
@@ -223,7 +223,7 @@ export class WrappedWebdavClient {
|
||||
const res = await this.client.customRequest(`/${this.vaultName}`, {
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Depth: "Infinity",
|
||||
Depth: "infinity",
|
||||
},
|
||||
responseType: "text",
|
||||
});
|
||||
@@ -523,15 +523,40 @@ export const deleteFromRemote = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedWebdavClient) => {
|
||||
export const checkConnectivity = async (
|
||||
client: WrappedWebdavClient,
|
||||
callbackFunc?: any
|
||||
) => {
|
||||
if (
|
||||
!(
|
||||
client.webdavConfig.address.startsWith("http://") ||
|
||||
client.webdavConfig.address.startsWith("https://")
|
||||
)
|
||||
) {
|
||||
const err = "Error: the url should start with http(s):// but it does not!";
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await client.init();
|
||||
const results = await getRemoteMeta(client, "/");
|
||||
if (results === undefined) {
|
||||
const err = "results is undefined";
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.debug(err);
|
||||
if (callbackFunc !== undefined) {
|
||||
callbackFunc(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
+319
-266
File diff suppressed because it is too large
Load Diff
+11
-6
@@ -323,7 +323,7 @@ const ensembleMixedStates = async (
|
||||
r = {
|
||||
key: entry.path,
|
||||
existLocal: true,
|
||||
mtimeLocal: Math.max(entry.stat.mtime, entry.stat.ctime),
|
||||
mtimeLocal: Math.max(entry.stat.mtime || 0, entry.stat.ctime || 0),
|
||||
sizeLocal: entry.stat.size,
|
||||
};
|
||||
} else if (entry instanceof TFolder) {
|
||||
@@ -450,22 +450,26 @@ const assignOperationToFileInplace = (
|
||||
// 0. find anything inconsistent
|
||||
if (r.existLocal && (r.mtimeLocal === undefined || r.mtimeLocal <= 0)) {
|
||||
throw Error(
|
||||
`Error: File ${r.key} has a last modified time <=0 or undefined in the local file system. It's abnormal and the plugin stops.`
|
||||
`Error: Anormal last modified time locally: ${JSON.stringify(r, null, 2)}`
|
||||
);
|
||||
}
|
||||
if (r.existRemote && (r.mtimeRemote === undefined || r.mtimeRemote <= 0)) {
|
||||
throw Error(
|
||||
`Error: File ${r.key} has a last modified time <=0 or undefined on the remote service. It's abnormal and the plugin stops.`
|
||||
`Error: Abnormal last modified time remotely: ${JSON.stringify(
|
||||
r,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (r.deltimeLocal !== undefined && r.deltimeLocal <= 0) {
|
||||
throw Error(
|
||||
`Error: File ${r.key} has a local deletion time <=0. It's abnormal and the plugin stops.`
|
||||
`Error: Abnormal deletion time locally: ${JSON.stringify(r, null, 2)}`
|
||||
);
|
||||
}
|
||||
if (r.deltimeRemote !== undefined && r.deltimeRemote <= 0) {
|
||||
throw Error(
|
||||
`Error: File ${r.key} has a remote deletion time <=0. It's abnormal and the plugin stops.`
|
||||
`Error: Abnormal deletion time remotely: ${JSON.stringify(r, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -984,7 +988,8 @@ const splitThreeSteps = (syncPlan: SyncPlanType, sortedKeys: string[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
// the deletionOps should be run from shadowest to deepest
|
||||
// the deletionOps should be run from max level to min level
|
||||
// right now it is sorted by level from min to max (NOT length of key!)
|
||||
// so we need to reverse it!
|
||||
deletionOps.reverse(); // inplace reverse
|
||||
|
||||
|
||||
+13
-19
@@ -1,5 +1,6 @@
|
||||
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import type RemotelySavePlugin from "./main"; // unavoidable
|
||||
import type { TransItemType } from "./i18n";
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
@@ -13,40 +14,33 @@ export class SyncAlgoV2Modal extends Modal {
|
||||
}
|
||||
onOpen() {
|
||||
let { contentEl } = this;
|
||||
const t = (x: TransItemType, vars?: any) => {
|
||||
return this.plugin.i18n.t(x, vars);
|
||||
};
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: "Remotely Save has a better sync algorithm",
|
||||
text: t("syncalgov2_title"),
|
||||
});
|
||||
|
||||
const texts = [
|
||||
"Welcome to use Remotely Save!",
|
||||
|
||||
"From version 0.3.0, a new algorithm has been developed, but it needs uploading extra meta data files _remotely-save-metadata-on-remote.{json,bin} to YOUR configured cloud destinations, besides your notes.",
|
||||
|
||||
"So that, for example, the second device can know that what files/folders have been deleted on the first device by reading those files.",
|
||||
|
||||
'If you agree, plase click the button "agree", and enjoy the plugin! AND PLEASE REMEMBER TO BACKUP YOUR VAULT FIRSTLY!',
|
||||
|
||||
'If you do not agree, you should stop using the current and later versions of Remotely Save. You could consider manually install the old version 0.2.14 which uses old algorithm and does not upload any extra meta data files. By clicking the "Do not agree" button, the plugin will unload itself, and you need to manually disable it in Obsidian settings.',
|
||||
];
|
||||
|
||||
const ul = contentEl.createEl("ul");
|
||||
|
||||
for (const t of texts) {
|
||||
t("syncalgov2_texts")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
ul.createEl("li", {
|
||||
text: t,
|
||||
text: val,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Agree");
|
||||
button.setButtonText(t("syncalgov2_button_agree"));
|
||||
button.onClick(async () => {
|
||||
this.agree = true;
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Do not agree");
|
||||
button.setButtonText(t("syncalgov2_button_disagree"));
|
||||
button.onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
encryptArrayBuffer,
|
||||
encryptStringToBase32,
|
||||
encryptStringToBase64url,
|
||||
getSizeFromEncToOrig,
|
||||
getSizeFromOrigToEnc,
|
||||
} from "../src/encrypt";
|
||||
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
|
||||
|
||||
@@ -110,4 +112,34 @@ describe("Encryption tests", () => {
|
||||
|
||||
expect(Buffer.from(dec).equals(Buffer.from(opensslArrBuf))).to.be.true;
|
||||
});
|
||||
|
||||
it("should get size from origin to encrypted correctly", () => {
|
||||
expect(() => getSizeFromOrigToEnc(-1)).to.throw();
|
||||
expect(() => getSizeFromOrigToEnc(0.5)).to.throw();
|
||||
expect(getSizeFromOrigToEnc(0)).equals(32);
|
||||
expect(getSizeFromOrigToEnc(15)).equals(32);
|
||||
expect(getSizeFromOrigToEnc(16)).equals(48);
|
||||
expect(getSizeFromOrigToEnc(31)).equals(48);
|
||||
expect(getSizeFromOrigToEnc(32)).equals(64);
|
||||
expect(getSizeFromOrigToEnc(14787203)).equals(14787232);
|
||||
});
|
||||
|
||||
it("should get size from encrypted to origin correctly", () => {
|
||||
expect(() => getSizeFromEncToOrig(-1)).to.throw();
|
||||
expect(() => getSizeFromEncToOrig(30)).to.throw();
|
||||
|
||||
expect(getSizeFromEncToOrig(32)).to.deep.equal({
|
||||
minSize: 0,
|
||||
maxSize: 15,
|
||||
});
|
||||
expect(getSizeFromEncToOrig(48)).to.deep.equal({
|
||||
minSize: 16,
|
||||
maxSize: 31,
|
||||
});
|
||||
|
||||
expect(() => getSizeFromEncToOrig(14787231)).to.throw();
|
||||
|
||||
let { minSize, maxSize } = getSizeFromEncToOrig(14787232);
|
||||
expect(minSize <= 14787203 && 14787203 <= maxSize).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
// "allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"0.3.2": "0.12.15",
|
||||
"0.3.7": "0.13.21"
|
||||
"0.3.10": "0.13.21"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user