Compare commits

..
18 Commits
Author SHA1 Message Date
fyears 6fce079481 bump to 0.3.19
Release A New Version / build (16.x) (push) Failing after 45s
2022-04-16 21:28:34 +08:00
fyears baf4e6b369 fix requrl for onedrive 2022-04-16 18:53:40 +08:00
fyears dd39e229d8 add password hide 2022-04-16 18:36:57 +08:00
fyears ba6e8be304 properly deal with 429 too many requests for dropbox 2022-04-16 17:02:07 +08:00
fyears cb7a372b69 properly reject emoji file names for dropbox 2022-04-16 11:36:04 +08:00
fyears 9a4dd0186d polish sync 2022-04-16 00:38:52 +08:00
fyears f9cdf323b8 fix cors 2022-04-13 22:24:58 +08:00
fyears 474216b9ab bump to 0.3.18
Release A New Version / build (16.x) (push) Failing after 48s
2022-04-10 17:39:12 +08:00
fyears 99f143115b export sync plans in table format 2022-04-10 17:38:37 +08:00
fyears 39940deb3f fix output 2022-04-10 11:54:34 +08:00
fyears df53ea957f add commands 2022-04-10 11:18:48 +08:00
fyears 0cf8023278 cast headers to lower cases 2022-04-10 11:05:37 +08:00
fyears be1386b345 update aws dep 2022-04-10 11:05:08 +08:00
fyears 9a189d63c9 add more desc for seafile 2022-04-09 17:29:29 +08:00
fyears 00d0c557df Merge pull request #67 from Paul1365972/patch-1
Seafile is indeed connectable
2022-04-09 17:25:57 +08:00
Paul1365972 a70a53eeec Seafile is indeed connectable 2022-04-08 18:51:01 +02:00
fyears 8cec2b00eb remove unused debug log 2022-04-09 00:18:48 +08:00
fyears 6efe52b26e some logs for onedrive 2022-04-09 00:18:23 +08:00
14 changed files with 416 additions and 100 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ The list is for information purposes only.
| [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? | | [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? |
| NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. | | NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
| OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. | | OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
| Seafile | Yes? | | Yes? | | Yes? | | | Seafile | Yes | | Yes | | Yes? | No CORS config by default. |
| `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. | | `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. |
| [Nutstore 坚果云](https://www.jianguoyun.com/) | Yes | | Yes | | Yes | No CORS support. | | [Nutstore 坚果云](https://www.jianguoyun.com/) | Yes | | Yes | | Yes | No CORS support. |
| [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. | | [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. |
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.3.17", "version": "0.3.19",
"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",
+10 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.3.17", "version": "0.3.19",
"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",
@@ -51,13 +51,13 @@
"webpack-cli": "^4.9.1" "webpack-cli": "^4.9.1"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.54.0", "@aws-sdk/client-s3": "^3.67.0",
"@aws-sdk/fetch-http-handler": "^3.54.0", "@aws-sdk/fetch-http-handler": "^3.58.0",
"@aws-sdk/lib-storage": "^3.54.0", "@aws-sdk/lib-storage": "^3.67.0",
"@aws-sdk/protocol-http": "^3.54.0", "@aws-sdk/protocol-http": "^3.58.0",
"@aws-sdk/querystring-builder": "^3.54.0", "@aws-sdk/querystring-builder": "^3.55.0",
"@aws-sdk/signature-v4-crt": "^3.54.0", "@aws-sdk/signature-v4-crt": "^3.66.0",
"@aws-sdk/types": "^3.54.0", "@aws-sdk/types": "^3.55.0",
"@azure/msal-node": "^1.6.0", "@azure/msal-node": "^1.6.0",
"@fyears/tsqueue": "^1.0.1", "@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.1", "@microsoft/microsoft-graph-client": "^3.0.1",
@@ -67,7 +67,9 @@
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"delay": "^5.0.0",
"dropbox": "^10.22.0", "dropbox": "^10.22.0",
"emoji-regex": "^10.1.0",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
+5 -2
View File
@@ -154,9 +154,12 @@ export interface FileOrFolderMixedState {
} }
export const API_VER_STAT_FOLDER = "0.13.27"; export const API_VER_STAT_FOLDER = "0.13.27";
export const API_VER_REQURL = "0.13.26"; export const API_VER_REQURL = "0.13.26"; // desktop ver 0.13.26, iOS ver 1.1.1
export const API_VER_REQURL_ANDROID = "0.14.6"; // Android ver 1.2.1
export const VALID_REQURL = export const VALID_REQURL =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp; (!Platform.isAndroidApp && requireApiVersion(API_VER_REQURL)) ||
(Platform.isAndroidApp && requireApiVersion(API_VER_REQURL_ANDROID));
export const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/"; export const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
+67 -1
View File
@@ -11,14 +11,73 @@ import {
DEFAULT_DEBUG_FOLDER, DEFAULT_DEBUG_FOLDER,
DEFAULT_LOG_HISTORY_FILE_PREFIX, DEFAULT_LOG_HISTORY_FILE_PREFIX,
DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX, DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX,
FileOrFolderMixedState,
} from "./baseTypes"; } from "./baseTypes";
import { log } from "./moreOnLog"; import { log } from "./moreOnLog";
const turnSyncPlanToTable = (record: string) => {
const syncPlan: SyncPlanType = JSON.parse(record);
const { ts, tsFmt, remoteType, mixedStates } = syncPlan;
type allowedHeadersType = keyof FileOrFolderMixedState;
const headers: allowedHeadersType[] = [
"key",
"remoteEncryptedKey",
"existLocal",
"sizeLocal",
"mtimeLocal",
"deltimeLocal",
"changeLocalMtimeUsingMapping",
"existRemote",
"sizeRemote",
"mtimeRemote",
"deltimeRemote",
"changeRemoteMtimeUsingMapping",
"decision",
"decisionBranch",
];
const lines = [
`ts: ${ts}${tsFmt !== undefined ? " / " + tsFmt : ""}`,
`remoteType: ${remoteType}`,
`| ${headers.join(" | ")} |`,
`| ${headers.map((x) => "---").join(" | ")} |`,
];
for (const [k1, v1] of Object.entries(syncPlan.mixedStates)) {
const k = k1 as string;
const v = v1 as FileOrFolderMixedState;
const singleLine = [];
for (const h of headers) {
const field = v[h];
if (field === undefined) {
singleLine.push("");
continue;
}
if (
h === "mtimeLocal" ||
h === "deltimeLocal" ||
h === "mtimeRemote" ||
h === "deltimeRemote"
) {
const fmt = v[(h + "Fmt") as allowedHeadersType] as string;
const s = `${field}${fmt !== undefined ? " / " + fmt : ""}`;
singleLine.push(s);
} else {
singleLine.push(field);
}
}
lines.push(`| ${singleLine.join(" | ")} |`);
}
return lines.join("\n");
};
export const exportVaultSyncPlansToFiles = async ( export const exportVaultSyncPlansToFiles = async (
db: InternalDBs, db: InternalDBs,
vault: Vault, vault: Vault,
vaultRandomID: string vaultRandomID: string,
toFormat: "table" | "json" = "json"
) => { ) => {
log.info("exporting"); log.info("exporting");
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault); await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
@@ -27,9 +86,16 @@ export const exportVaultSyncPlansToFiles = async (
if (records.length === 0) { if (records.length === 0) {
md = "No sync plans history found"; md = "No sync plans history found";
} else { } else {
if (toFormat === "json") {
md = md =
"Sync plans found:\n\n" + "Sync plans found:\n\n" +
records.map((x) => "```json\n" + x + "\n```\n").join("\n"); records.map((x) => "```json\n" + x + "\n```\n").join("\n");
} else if (toFormat === "table") {
md =
"Sync plans found:\n\n" + records.map(turnSyncPlanToTable).join("\n\n");
} else {
const _: never = toFormat;
}
} }
const ts = Date.now(); const ts = Date.now();
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`; const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
+58 -3
View File
@@ -8,7 +8,7 @@ import {
FileSystemAdapter, FileSystemAdapter,
} from "obsidian"; } from "obsidian";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { createElement, RotateCcw, RefreshCcw } from "lucide"; import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes"; import type { RemotelySavePluginSettings } from "./baseTypes";
import { import {
COMMAND_CALLBACK, COMMAND_CALLBACK,
@@ -57,6 +57,10 @@ import { applyPresetRulesInplace } from "./presetRules";
import { applyLogWriterInplace, log } from "./moreOnLog"; import { applyLogWriterInplace, log } from "./moreOnLog";
import AggregateError from "aggregate-error"; import AggregateError from "aggregate-error";
import {
exportVaultLoggerOutputToFiles,
exportVaultSyncPlansToFiles,
} from "./debugMode";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG, s3: DEFAULT_S3_CONFIG,
@@ -89,6 +93,7 @@ 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 iconNameLogs = `remotely-save-logs`;
const getIconSvg = () => { const getIconSvg = () => {
const iconSvgSyncWait = createElement(RotateCcw); const iconSvgSyncWait = createElement(RotateCcw);
@@ -97,13 +102,18 @@ const getIconSvg = () => {
const iconSvgSyncRunning = createElement(RefreshCcw); const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("width", "100"); iconSvgSyncRunning.setAttribute("width", "100");
iconSvgSyncRunning.setAttribute("height", "100"); iconSvgSyncRunning.setAttribute("height", "100");
const iconSvgLogs = createElement(FileText);
iconSvgLogs.setAttribute("width", "100");
iconSvgLogs.setAttribute("height", "100");
const res = { const res = {
iconSvgSyncWait: iconSvgSyncWait.outerHTML, iconSvgSyncWait: iconSvgSyncWait.outerHTML,
iconSvgSyncRunning: iconSvgSyncRunning.outerHTML, iconSvgSyncRunning: iconSvgSyncRunning.outerHTML,
iconSvgLogs: iconSvgLogs.outerHTML,
}; };
iconSvgSyncWait.empty(); iconSvgSyncWait.empty();
iconSvgSyncRunning.empty(); iconSvgSyncRunning.empty();
iconSvgLogs.empty();
return res; return res;
}; };
@@ -204,7 +214,7 @@ export default class RemotelySavePlugin extends Plugin {
() => self.saveSettings() () => self.saveSettings()
); );
const remoteRsp = await client.listFromRemote(); const remoteRsp = await client.listFromRemote();
log.debug(remoteRsp); // log.debug(remoteRsp);
getNotice( getNotice(
t("syncrun_step3", { t("syncrun_step3", {
@@ -369,10 +379,11 @@ export default class RemotelySavePlugin extends Plugin {
async onload() { async onload() {
log.info(`loading plugin ${this.manifest.id}`); log.info(`loading plugin ${this.manifest.id}`);
const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg(); const { iconSvgSyncWait, iconSvgSyncRunning, iconSvgLogs } = getIconSvg();
addIcon(iconNameSyncWait, iconSvgSyncWait); addIcon(iconNameSyncWait, iconSvgSyncWait);
addIcon(iconNameSyncRunning, iconSvgSyncRunning); addIcon(iconNameSyncRunning, iconSvgSyncRunning);
addIcon(iconNameLogs, iconSvgLogs);
this.oauth2Info = { this.oauth2Info = {
verifier: "", verifier: "",
@@ -664,6 +675,50 @@ export default class RemotelySavePlugin extends Plugin {
}, },
}); });
this.addCommand({
id: "export-sync-plans-json",
name: t("command_exportsyncplans_json"),
icon: iconNameLogs,
callback: async () => {
await exportVaultSyncPlansToFiles(
this.db,
this.app.vault,
this.vaultRandomID,
"json"
);
new Notice(t("settings_syncplans_notice"));
},
});
this.addCommand({
id: "export-sync-plans-table",
name: t("command_exportsyncplans_table"),
icon: iconNameLogs,
callback: async () => {
await exportVaultSyncPlansToFiles(
this.db,
this.app.vault,
this.vaultRandomID,
"table"
);
new Notice(t("settings_syncplans_notice"));
},
});
this.addCommand({
id: "export-logs-in-db",
name: t("command_exportlogsindb"),
icon: iconNameLogs,
callback: async () => {
await exportVaultLoggerOutputToFiles(
this.db,
this.app.vault,
this.vaultRandomID
);
new Notice(t("settings_logtodbexport_notice"));
},
});
this.addSettingTab(new RemotelySaveSettingTab(this.app, this)); this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
// this.registerDomEvent(document, "click", (evt: MouseEvent) => { // this.registerDomEvent(document, "click", (evt: MouseEvent) => {
+29
View File
@@ -3,6 +3,7 @@ import * as path from "path";
import { base32, base64url } from "rfc4648"; import { base32, base64url } from "rfc4648";
import XRegExp from "xregexp"; import XRegExp from "xregexp";
import emojiRegex from "emoji-regex";
import { log } from "./moreOnLog"; import { log } from "./moreOnLog";
@@ -162,6 +163,34 @@ export const isVaildText = (a: string) => {
); );
}; };
/**
* Use regex to detect a text contains emoji or not.
* @param a
* @returns
*/
export const hasEmojiInText = (a: string) => {
const regex = emojiRegex();
return regex.test(a);
};
/**
* Convert the headers to a normal object.
* @param h
* @param toLower
* @returns
*/
export const headersToRecord = (h: Headers, toLower: boolean = true) => {
const res: Record<string, string> = {};
h.forEach((v, k) => {
if (toLower) {
res[k.toLowerCase()] = v;
} else {
res[k] = v;
}
});
return res;
};
/** /**
* If input is already a folder, returns it as is; * If input is already a folder, returns it as is;
* And if input is a file, returns its direname. * And if input is a file, returns its direname.
+135 -17
View File
@@ -1,4 +1,6 @@
import { Dropbox, DropboxAuth, files } from "dropbox"; import delay from "delay";
import { Dropbox, DropboxAuth } from "dropbox";
import type { files, DropboxResponseError, DropboxResponse } from "dropbox";
import { Vault } from "obsidian"; import { Vault } from "obsidian";
import * as path from "path"; import * as path from "path";
import { import {
@@ -8,7 +10,13 @@ import {
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
} from "./baseTypes"; } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { bufferToArrayBuffer, getFolderLevels, mkdirpInVault } from "./misc"; import {
bufferToArrayBuffer,
getFolderLevels,
hasEmojiInText,
headersToRecord,
mkdirpInVault,
} from "./misc";
export { Dropbox } from "dropbox"; export { Dropbox } from "dropbox";
@@ -270,6 +278,73 @@ export const setConfigBySuccessfullAuthInplace = async (
// Other usual common methods // Other usual common methods
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
interface ErrSubType {
error: {
retry_after: number;
};
}
async function retryReq<T>(
reqFunc: () => Promise<DropboxResponse<T>>,
extraHint: string = ""
): Promise<DropboxResponse<T>> {
const waitSeconds = [1, 2, 4, 8]; // hard code exponential backoff
for (let idx = 0; idx < waitSeconds.length; ++idx) {
try {
if (idx !== 0) {
log.warn(
`${extraHint === "" ? "" : extraHint + ": "}The ${
idx + 1
}-th try starts at time ${Date.now()}`
);
}
return await reqFunc();
} catch (e: unknown) {
const err = e as DropboxResponseError<ErrSubType>;
if (err.status === undefined) {
// then the err is not DropboxResponseError
throw err;
}
if (err.status !== 429) {
// then the err is not "too many requests", give up
throw err;
}
if (idx === waitSeconds.length - 1) {
// the last retry also failed, give up
throw new Error(
`${
extraHint === "" ? "" : extraHint + ": "
}"429 too many requests", after retrying for ${
idx + 1
} times still failed.`
);
}
const headers = headersToRecord(err.headers);
const svrSec =
err.error.error.retry_after ||
parseInt(headers["retry-after"] || "1") ||
1;
const fallbackSec = waitSeconds[idx];
const secMin = Math.max(svrSec, fallbackSec);
const secMax = Math.max(secMin * 1.8, 2);
log.warn(
`${
extraHint === "" ? "" : extraHint + ": "
}We have "429 too many requests" error of ${
idx + 1
}-th try, at time ${Date.now()}, and wait for ${secMin} ~ ${secMax} seconds to retry. Original info: ${JSON.stringify(
err.error,
null,
2
)}`
);
await delay.range(secMin * 1000, secMax * 1000);
}
}
}
export class WrappedDropboxClient { export class WrappedDropboxClient {
dropboxConfig: DropboxConfig; dropboxConfig: DropboxConfig;
remoteBaseDir: string; remoteBaseDir: string;
@@ -338,6 +413,13 @@ export class WrappedDropboxClient {
} }
if (!this.vaultFolderExists) { if (!this.vaultFolderExists) {
log.info(`remote does not have folder /${this.remoteBaseDir}`); log.info(`remote does not have folder /${this.remoteBaseDir}`);
if (hasEmojiInText(`/${this.remoteBaseDir}`)) {
throw new Error(
`/${this.remoteBaseDir}: Error: Dropbox does not support emoji in folder names.`
);
}
await this.dropbox.filesCreateFolderV2({ await this.dropbox.filesCreateFolderV2({
path: `/${this.remoteBaseDir}`, path: `/${this.remoteBaseDir}`,
}); });
@@ -377,10 +459,12 @@ export const getRemoteMeta = async (
// filesGetMetadata doesn't support root folder // filesGetMetadata doesn't support root folder
// we instead try to list files // we instead try to list files
// if no error occurs, we ensemble a fake result. // if no error occurs, we ensemble a fake result.
const rsp = await client.dropbox.filesListFolder({ const rsp = await retryReq(() =>
client.dropbox.filesListFolder({
path: `/${client.remoteBaseDir}`, path: `/${client.remoteBaseDir}`,
recursive: false, // don't need to recursive here recursive: false, // don't need to recursive here
}); })
);
if (rsp.status !== 200) { if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp)); throw Error(JSON.stringify(rsp));
} }
@@ -395,9 +479,11 @@ export const getRemoteMeta = async (
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir); const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesGetMetadata({ const rsp = await retryReq(() =>
client.dropbox.filesGetMetadata({
path: key, path: key,
}); })
);
if (rsp.status !== 200) { if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp)); throw Error(JSON.stringify(rsp));
} }
@@ -423,6 +509,12 @@ export const uploadToRemote = async (
} }
uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir); uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir);
if (hasEmojiInText(uploadFile)) {
throw new Error(
`${uploadFile}: Error: Dropbox does not support emoji in file / folder names.`
);
}
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) { if (isFolder && isRecursively) {
@@ -438,11 +530,19 @@ export const uploadToRemote = async (
// created, pass // created, pass
} else { } else {
try { try {
await client.dropbox.filesCreateFolderV2({ await retryReq(
() =>
client.dropbox.filesCreateFolderV2({
path: uploadFile, path: uploadFile,
}); }),
fileOrFolderPath
);
foldersCreatedBefore?.add(uploadFile); foldersCreatedBefore?.add(uploadFile);
} catch (err) { } catch (e: unknown) {
const err = e as DropboxResponseError<files.CreateFolderError>;
if (err.status === undefined) {
throw err;
}
if (err.status === 409) { if (err.status === 409) {
// pass // pass
foldersCreatedBefore?.add(uploadFile); foldersCreatedBefore?.add(uploadFile);
@@ -455,10 +555,14 @@ export const uploadToRemote = async (
return res; return res;
} else { } else {
// if encrypted, upload a fake file with the encrypted file name // if encrypted, upload a fake file with the encrypted file name
await client.dropbox.filesUpload({ await retryReq(
() =>
client.dropbox.filesUpload({
path: uploadFile, path: uploadFile,
contents: "", contents: "",
}); }),
fileOrFolderPath
);
return await getRemoteMeta(client, uploadFile); return await getRemoteMeta(client, uploadFile);
} }
} else { } else {
@@ -480,13 +584,19 @@ export const uploadToRemote = async (
} }
// in dropbox, we don't need to create folders before uploading! cool! // in dropbox, we don't need to create folders before uploading! cool!
// TODO: filesUploadSession for larger files (>=150 MB) // TODO: filesUploadSession for larger files (>=150 MB)
await client.dropbox.filesUpload({
await retryReq(
() =>
client.dropbox.filesUpload({
path: uploadFile, path: uploadFile,
contents: remoteContent, contents: remoteContent,
mode: { mode: {
".tag": "overwrite", ".tag": "overwrite",
}, },
}); }),
fileOrFolderPath
);
// we want to mark that parent folders are created // we want to mark that parent folders are created
if (foldersCreatedBefore !== undefined) { if (foldersCreatedBefore !== undefined) {
const dirs = getFolderLevels(uploadFile).map((x) => const dirs = getFolderLevels(uploadFile).map((x) =>
@@ -554,9 +664,13 @@ const downloadFromRemoteRaw = async (
) => { ) => {
await client.init(); await client.init();
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir); const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesDownload({ const rsp = await retryReq(
() =>
client.dropbox.filesDownload({
path: key, path: key,
}); }),
fileOrFolderPath
);
if ((rsp.result as any).fileBlob !== undefined) { if ((rsp.result as any).fileBlob !== undefined) {
// we get a Blob // we get a Blob
const content = (rsp.result as any).fileBlob as Blob; const content = (rsp.result as any).fileBlob as Blob;
@@ -631,9 +745,13 @@ export const deleteFromRemote = async (
await client.init(); await client.init();
try { try {
await client.dropbox.filesDeleteV2({ await retryReq(
() =>
client.dropbox.filesDeleteV2({
path: remoteFileName, path: remoteFileName,
}); }),
fileOrFolderPath
);
} catch (err) { } catch (err) {
console.error("some error while deleting"); console.error("some error while deleting");
console.error(err); console.error(err);
+7 -6
View File
@@ -8,7 +8,7 @@ import type {
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { request, requestUrl, requireApiVersion, Vault } from "obsidian"; import { request, requestUrl, requireApiVersion, Vault } from "obsidian";
import { import {
API_VER_REQURL, VALID_REQURL,
COMMAND_CALLBACK_ONEDRIVE, COMMAND_CALLBACK_ONEDRIVE,
DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE,
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
@@ -400,7 +400,7 @@ export class WrappedOnedriveClient {
// log.info(`already checked, /${this.remoteBaseDir} exist before`) // log.info(`already checked, /${this.remoteBaseDir} exist before`)
} else { } else {
const k = await this.getJson("/drive/special/approot/children"); const k = await this.getJson("/drive/special/approot/children");
log.debug(k); // log.debug(k);
this.vaultFolderExists = this.vaultFolderExists =
(k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir) (k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir)
.length > 0; .length > 0;
@@ -484,7 +484,7 @@ export class WrappedOnedriveClient {
deleteJson = async (pathFragOrig: string) => { deleteJson = async (pathFragOrig: string) => {
const theUrl = this.buildUrl(pathFragOrig); const theUrl = this.buildUrl(pathFragOrig);
log.debug(`deleteJson, theUrl=${theUrl}`); log.debug(`deleteJson, theUrl=${theUrl}`);
if (requireApiVersion(API_VER_REQURL)) { if (VALID_REQURL) {
await requestUrl({ await requestUrl({
url: theUrl, url: theUrl,
method: "DELETE", method: "DELETE",
@@ -508,7 +508,7 @@ export class WrappedOnedriveClient {
// TODO: // TODO:
// 20220401: On Android, requestUrl has issue that text becomes base64. // 20220401: On Android, requestUrl has issue that text becomes base64.
// Use fetch everywhere instead! // Use fetch everywhere instead!
if (false /*requireApiVersion(API_VER_REQURL)*/) { if (false /*VALID_REQURL*/) {
await requestUrl({ await requestUrl({
url: theUrl, url: theUrl,
method: "PUT", method: "PUT",
@@ -556,7 +556,7 @@ export class WrappedOnedriveClient {
// TODO: // TODO:
// 20220401: On Android, requestUrl has issue that text becomes base64. // 20220401: On Android, requestUrl has issue that text becomes base64.
// Use fetch everywhere instead! // Use fetch everywhere instead!
if (false /*requireApiVersion(API_VER_REQURL)*/) { if (false /*VALID_REQURL*/) {
const res = await requestUrl({ const res = await requestUrl({
url: theUrl, url: theUrl,
method: "PUT", method: "PUT",
@@ -617,6 +617,7 @@ export const listFromRemote = async (
`/drive/special/approot:/${client.remoteBaseDir}:/delta` `/drive/special/approot:/${client.remoteBaseDir}:/delta`
); );
let driveItems = res.value as DriveItem[]; let driveItems = res.value as DriveItem[];
// log.debug(driveItems);
while (NEXT_LINK_KEY in res) { while (NEXT_LINK_KEY in res) {
res = await client.getJson(res[NEXT_LINK_KEY]); res = await client.getJson(res[NEXT_LINK_KEY]);
@@ -807,7 +808,7 @@ const downloadFromRemoteRaw = async (
`${key}?$select=@microsoft.graph.downloadUrl` `${key}?$select=@microsoft.graph.downloadUrl`
); );
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"]; const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
if (requireApiVersion(API_VER_REQURL)) { if (VALID_REQURL) {
const content = (await requestUrl({ url: downloadUrl })).arrayBuffer; const content = (await requestUrl({ url: downloadUrl })).arrayBuffer;
return content; return content;
} else { } else {
+6 -1
View File
@@ -109,6 +109,11 @@ class ObsHttpHandler extends FetchHttpHandler {
const raceOfPromises = [ const raceOfPromises = [
requestUrl(param).then((rsp) => { requestUrl(param).then((rsp) => {
const headers = rsp.headers;
const headersLower: Record<string, string> = {};
for (const key of Object.keys(headers)) {
headersLower[key.toLowerCase()] = headers[key];
}
const stream = new ReadableStream<Uint8Array>({ const stream = new ReadableStream<Uint8Array>({
start(controller) { start(controller) {
controller.enqueue(new Uint8Array(rsp.arrayBuffer)); controller.enqueue(new Uint8Array(rsp.arrayBuffer));
@@ -117,7 +122,7 @@ class ObsHttpHandler extends FetchHttpHandler {
}); });
return { return {
response: new HttpResponse({ response: new HttpResponse({
headers: rsp.headers, headers: headersLower,
statusCode: rsp.status, statusCode: rsp.status,
body: stream, body: stream,
}), }),
+66 -35
View File
@@ -7,6 +7,8 @@ import {
Platform, Platform,
requireApiVersion, requireApiVersion,
} from "obsidian"; } from "obsidian";
import type { TextComponent } from "obsidian";
import { createElement, Eye, EyeOff } from "lucide";
import { import {
API_VER_REQURL, API_VER_REQURL,
DEFAULT_DEBUG_FOLDER, DEFAULT_DEBUG_FOLDER,
@@ -627,8 +629,33 @@ class ExportSettingsQrCodeModal extends Modal {
} }
} }
const getEyesElements = () => {
const eyeEl = createElement(Eye);
const eyeOffEl = createElement(EyeOff);
return {
eye: eyeEl.outerHTML,
eyeOff: eyeOffEl.outerHTML,
};
};
const wrapTextWithPasswordHide = (text: TextComponent) => {
const { eye, eyeOff } = getEyesElements();
const hider = text.inputEl.insertAdjacentElement("afterend", createSpan());
// the init type of hider is "hidden" === eyeOff === password
hider.innerHTML = eyeOff;
hider.addEventListener("click", (e) => {
const isText = text.inputEl.getAttribute("type") === "text";
text.inputEl.setAttribute("type", isText ? "password" : "text");
hider.innerHTML = isText ? eyeOff : eye;
});
// the init type of text el is password
text.inputEl.setAttribute("type", "password");
return text;
};
export class RemotelySaveSettingTab extends PluginSettingTab { export class RemotelySaveSettingTab extends PluginSettingTab {
plugin: RemotelySavePlugin; readonly plugin: RemotelySavePlugin;
constructor(app: App, plugin: RemotelySavePlugin) { constructor(app: App, plugin: RemotelySavePlugin) {
super(app, plugin); super(app, plugin);
@@ -658,14 +685,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
new Setting(passwordDiv) new Setting(passwordDiv)
.setName(t("settings_password")) .setName(t("settings_password"))
.setDesc(t("settings_password_desc")) .setDesc(t("settings_password_desc"))
.addText((text) => .addText((text) => {
wrapTextWithPasswordHide(text);
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(`${this.plugin.settings.password}`) .setValue(`${this.plugin.settings.password}`)
.onChange(async (value) => { .onChange(async (value) => {
newPassword = value.trim(); newPassword = value.trim();
});
}) })
)
.addButton(async (button) => { .addButton(async (button) => {
button.setButtonText(t("confirm")); button.setButtonText(t("confirm"));
button.onClick(async () => { button.onClick(async () => {
@@ -816,29 +844,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
new Setting(s3Div) new Setting(s3Div)
.setName(t("settings_s3_accesskeyid")) .setName(t("settings_s3_accesskeyid"))
.setDesc(t("settings_s3_accesskeyid")) .setDesc(t("settings_s3_accesskeyid_desc"))
.addText((text) => .addText((text) => {
wrapTextWithPasswordHide(text);
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(`${this.plugin.settings.s3.s3AccessKeyID}`) .setValue(`${this.plugin.settings.s3.s3AccessKeyID}`)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.s3.s3AccessKeyID = value.trim(); this.plugin.settings.s3.s3AccessKeyID = value.trim();
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) });
); });
new Setting(s3Div) new Setting(s3Div)
.setName(t("settings_s3_secretaccesskey")) .setName(t("settings_s3_secretaccesskey"))
.setDesc(t("settings_s3_secretaccesskey")) .setDesc(t("settings_s3_secretaccesskey_desc"))
.addText((text) => .addText((text) => {
wrapTextWithPasswordHide(text);
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(`${this.plugin.settings.s3.s3SecretAccessKey}`) .setValue(`${this.plugin.settings.s3.s3SecretAccessKey}`)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.s3.s3SecretAccessKey = value.trim(); this.plugin.settings.s3.s3SecretAccessKey = value.trim();
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) });
); });
new Setting(s3Div) new Setting(s3Div)
.setName(t("settings_s3_bucketname")) .setName(t("settings_s3_bucketname"))
@@ -877,11 +907,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
if (VALID_REQURL) { if (VALID_REQURL) {
new Setting(s3Div) new Setting(s3Div)
.setName(t("settings_s3_bypasscorslocally")) .setName(t("settings_s3_bypasscorslocally"))
.setDesc( .setDesc(t("settings_s3_bypasscorslocally_desc"))
t("settings_s3_bypasscorslocally_desc", {
ver: API_VER_REQURL,
})
)
.addDropdown((dropdown) => { .addDropdown((dropdown) => {
dropdown dropdown
.addOption("disable", t("disable")) .addOption("disable", t("disable"))
@@ -1297,15 +1323,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
if (!VALID_REQURL) { if (!VALID_REQURL) {
if (Platform.isAndroidApp) {
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: t("settings_webdav_cors_android"), text: t("settings_webdav_cors_os"),
}); });
} else {
webdavDiv.createEl("p", {
text: t("settings_webdav_cors_otheros"),
});
}
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: t("settings_webdav_cors"), text: t("settings_webdav_cors"),
@@ -1346,7 +1366,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
new Setting(webdavDiv) new Setting(webdavDiv)
.setName(t("settings_webdav_user")) .setName(t("settings_webdav_user"))
.setDesc(t("settings_webdav_user_desc")) .setDesc(t("settings_webdav_user_desc"))
.addText((text) => .addText((text) => {
wrapTextWithPasswordHide(text);
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(this.plugin.settings.webdav.username) .setValue(this.plugin.settings.webdav.username)
@@ -1359,13 +1380,14 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.webdav.depth = "auto_unknown"; this.plugin.settings.webdav.depth = "auto_unknown";
} }
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) });
); });
new Setting(webdavDiv) new Setting(webdavDiv)
.setName(t("settings_webdav_password")) .setName(t("settings_webdav_password"))
.setDesc(t("settings_webdav_password_desc")) .setDesc(t("settings_webdav_password_desc"))
.addText((text) => .addText((text) => {
wrapTextWithPasswordHide(text);
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(this.plugin.settings.webdav.password) .setValue(this.plugin.settings.webdav.password)
@@ -1378,8 +1400,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.webdav.depth = "auto_unknown"; this.plugin.settings.webdav.depth = "auto_unknown";
} }
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) });
); });
new Setting(webdavDiv) new Setting(webdavDiv)
.setName(t("settings_webdav_auth")) .setName(t("settings_webdav_auth"))
@@ -1678,11 +1700,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
button.setButtonText(t("settings_outputsettingsconsole_button")); button.setButtonText(t("settings_outputsettingsconsole_button"));
button.onClick(async () => { button.onClick(async () => {
const c = messyConfigToNormal(await this.plugin.loadData()); const c = messyConfigToNormal(await this.plugin.loadData());
if (c.currLogLevel === "debug") {
// no need to ouput it again because debug mode already output it
} else {
log.info(c); log.info(c);
}
new Notice(t("settings_outputsettingsconsole_notice")); new Notice(t("settings_outputsettingsconsole_notice"));
}); });
}); });
@@ -1691,12 +1709,25 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setName(t("settings_syncplans")) .setName(t("settings_syncplans"))
.setDesc(t("settings_syncplans_desc")) .setDesc(t("settings_syncplans_desc"))
.addButton(async (button) => { .addButton(async (button) => {
button.setButtonText(t("settings_syncplans_button")); button.setButtonText(t("settings_syncplans_button_json"));
button.onClick(async () => { button.onClick(async () => {
await exportVaultSyncPlansToFiles( await exportVaultSyncPlansToFiles(
this.plugin.db, this.plugin.db,
this.app.vault, this.app.vault,
this.plugin.vaultRandomID this.plugin.vaultRandomID,
"json"
);
new Notice(t("settings_syncplans_notice"));
});
})
.addButton(async (button) => {
button.setButtonText(t("settings_syncplans_button_table"));
button.onClick(async () => {
await exportVaultSyncPlansToFiles(
this.plugin.db,
this.app.vault,
this.plugin.vaultRandomID,
"table"
); );
new Notice(t("settings_syncplans_notice")); new Notice(t("settings_syncplans_notice"));
}); });
+9 -3
View File
@@ -459,9 +459,15 @@ const ensembleMixedStates = async (
results[key].changeLocalMtimeUsingMapping = results[key].changeLocalMtimeUsingMapping =
r.changeLocalMtimeUsingMapping; r.changeLocalMtimeUsingMapping;
} else { } else {
results[key] = r; // So, the file doesn't exist,
results[key].existLocal = false; // we have already checked local // except that it existed in the "renamed to" history records.
results[key].existRemote = false; // we have already checked remote // Most likely because that the user deleted the file while Obsidian was closed,
// so Obsidian could not track the deletions.
// We are not sure how to deal with this, so do not generate anything here!
// // // The following 3 lines are of old logic, and have been removed:
// // results[key] = r;
// // results[key].existLocal = false; // we have already checked local
// // results[key].existRemote = false; // we have already checked remote
} }
} else { } else {
throw Error( throw Error(
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"0.3.2": "0.12.15", "0.3.2": "0.12.15",
"0.3.17": "0.13.21" "0.3.19": "0.13.21"
} }