From c04876e0c5835982f10743c17f8f29c6fe4eb793 Mon Sep 17 00:00:00 2001 From: fyears <1142836+fyears@users.noreply.github.com> Date: Thu, 10 Mar 2022 23:54:35 +0800 Subject: [PATCH] A big commit Squashed commit of CORS: commit 8cffa38ebae2a46b7c8e855c7b21a124e35adc89 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Thu Mar 10 23:52:56 2022 +0800 bypass more cors for onedrive commit 1b59ac1e58032099068aab55d22ef96c6396f203 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 23:58:28 2022 +0800 change wordings for webdav cors commit 73142eb18b59fff20839680e866f51cfcb0a6226 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 23:38:58 2022 +0800 remove cors hint for webdav commit 7dbb0b49d50e529b2b72e55ea2c8503ba7fa9268 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 23:31:54 2022 +0800 fix webdav commit c28c4e19720a56230d483acf306463d42e619fa4 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 23:31:35 2022 +0800 remove more headers commit 4eeae7043fa68d669a5c23c5549c14c1260ce638 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 23:18:32 2022 +0800 polish cors hints for s3 commit d9e55a91a1c413e9419cd6b30a3a6e3b403d483b Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 22:40:37 2022 +0800 fix format commit b780a3eb4e37b05b8e8b92d6a2f9283b3459d738 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 22:37:02 2022 +0800 finally correctly inject requestUrl into s3 commit 6a55a1a43d7653d65579ab88aa816e5d54cd276a Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 22:33:18 2022 +0800 to arraybuffer from view commit 2f2607b4f0a3d9db5943528ced57cb2fdb419607 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Wed Mar 9 13:31:22 2022 +0800 add split ranges commit ea24da24dea83fdb770e7e391cf8a2e4fea78d0d Author: fyears <1142836+fyears@users.noreply.github.com> Date: Sun Mar 6 22:57:50 2022 +0800 add settings of bypassing for s3 commit 2f099dc8ca1e66ea137b28dd329be50968734ba6 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Sun Mar 6 22:38:07 2022 +0800 use api ver var commit 74c7ce2449a88cbe7c7f50cbb687b36ff3732c04 Author: fyears <1142836+fyears@users.noreply.github.com> Date: Sun Mar 6 22:37:25 2022 +0800 correct way to inject s3 commit f29945d73132d21b2c44472ec2cafc06b9d71e8f Author: fyears <1142836+fyears@users.noreply.github.com> Date: Tue Mar 1 00:09:57 2022 +0800 add new http handler commit d55104cb08e168cbcc243cf901cbd7f46f2e324b Author: fyears <1142836+fyears@users.noreply.github.com> Date: Mon Feb 28 22:59:55 2022 +0800 add types for patch commit 50b79ade7188ee7dfab9c1d03119585db358ba6f Author: fyears <1142836+fyears@users.noreply.github.com> Date: Mon Feb 28 08:25:19 2022 +0800 remove verbose commit 83f0e71aa15aa7586f6d4e105cd77c25c2e113ce Author: fyears <1142836+fyears@users.noreply.github.com> Date: Mon Feb 28 08:25:04 2022 +0800 patch webdav! --- package.json | 7 +- src/baseTypes.ts | 2 + src/main.ts | 2 +- src/misc.ts | 32 +++++++- src/remoteForOnedrive.ts | 140 +++++++++++++++++++++------------- src/remoteForS3.ts | 161 +++++++++++++++++++++++++++++++++++---- src/remoteForWebdav.ts | 113 +++++++++++++++++++++++++-- src/settings.ts | 83 ++++++++++++++++---- tests/misc.test.ts | 53 +++++++++++++ 9 files changed, 503 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index c5c0a6d..a7e8adb 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,12 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.37.0", - "@aws-sdk/lib-storage": "^3.40.1", + "@aws-sdk/fetch-http-handler": "^3.53.0", + "@aws-sdk/lib-storage": "^3.53.1", + "@aws-sdk/protocol-http": "^3.53.0", + "@aws-sdk/querystring-builder": "^3.53.0", "@aws-sdk/signature-v4-crt": "^3.37.0", + "@aws-sdk/types": "^3.53.0", "@azure/msal-node": "^1.6.0", "@fyears/tsqueue": "^1.0.1", "@microsoft/microsoft-graph-client": "^3.0.1", @@ -65,6 +69,7 @@ "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", diff --git a/src/baseTypes.ts b/src/baseTypes.ts index 72f24d7..dd8719a 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -11,6 +11,7 @@ export interface S3Config { s3AccessKeyID: string; s3SecretAccessKey: string; s3BucketName: string; + bypassCorsLocally?: boolean; } export interface DropboxConfig { @@ -117,3 +118,4 @@ export interface FileOrFolderMixedState { } export const API_VER_STAT_FOLDER = "0.13.27"; +export const API_VER_REQURL = "0.13.26"; diff --git a/src/main.ts b/src/main.ts index 2b2a1a0..02c1575 100644 --- a/src/main.ts +++ b/src/main.ts @@ -154,7 +154,7 @@ export default class RemotelySavePlugin extends Plugin { () => self.saveSettings() ); const remoteRsp = await client.listFromRemote(); - log.info(remoteRsp); + log.debug(remoteRsp); getNotice(`3/${MAX_STEPS} Checking password correct or not.`); this.syncStatus = "checking_password"; diff --git a/src/misc.ts b/src/misc.ts index ed00ca7..712a9f8 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -79,7 +79,9 @@ export const mkdirpInVault = async (thePath: string, vault: Vault) => { * @param b Buffer * @returns ArrayBuffer */ -export const bufferToArrayBuffer = (b: Buffer | Uint8Array) => { +export const bufferToArrayBuffer = ( + b: Buffer | Uint8Array | ArrayBufferView +) => { return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength); }; @@ -230,3 +232,31 @@ export const getRandomArrayBuffer = (byteLength: number) => { export const reverseString = (x: string) => { return [...x].reverse().join(""); }; + +export interface SplitRange { + partNum: number; // startting from 1 + start: number; + end: number; // exclusive +} +export const getSplitRanges = (bytesTotal: number, bytesEachPart: number) => { + const res: SplitRange[] = []; + if (bytesEachPart >= bytesTotal) { + res.push({ + partNum: 1, + start: 0, + end: bytesTotal, + }); + return res; + } + const remainder = bytesTotal % bytesEachPart; + const howMany = + Math.floor(bytesTotal / bytesEachPart) + (remainder === 0 ? 0 : 1); + for (let i = 0; i < howMany; ++i) { + res.push({ + partNum: i + 1, + start: bytesEachPart * i, + end: Math.min(bytesEachPart * (i + 1), bytesTotal), + }); + } + return res; +}; diff --git a/src/remoteForOnedrive.ts b/src/remoteForOnedrive.ts index bf82f24..dd1cea6 100644 --- a/src/remoteForOnedrive.ts +++ b/src/remoteForOnedrive.ts @@ -7,8 +7,9 @@ import type { } from "@microsoft/microsoft-graph-types"; import cloneDeep from "lodash/cloneDeep"; import * as origLog from "loglevel"; -import { request, Vault } from "obsidian"; +import { request, requestUrl, requireApiVersion, Vault } from "obsidian"; import { + API_VER_REQURL, COMMAND_CALLBACK_ONEDRIVE, OAUTH2_FORCE_EXPIRE_MILLISECONDS, OnedriveConfig, @@ -482,28 +483,45 @@ export class WrappedOnedriveClient { deleteJson = async (pathFragOrig: string) => { const theUrl = this.buildUrl(pathFragOrig); log.debug(`deleteJson, theUrl=${theUrl}`); - // TODO: delete does not have response, so Obsidian request may have error - // currently downgraded to fetch()! - await fetch(theUrl, { - method: "DELETE", - headers: { - Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, - }, - }); + if (requireApiVersion(API_VER_REQURL)) { + await requestUrl({ + url: theUrl, + method: "DELETE", + headers: { + Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, + }, + }); + } else { + await fetch(theUrl, { + method: "DELETE", + headers: { + Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, + }, + }); + } }; putArrayBuffer = async (pathFragOrig: string, payload: ArrayBuffer) => { const theUrl = this.buildUrl(pathFragOrig); log.debug(`putArrayBuffer, theUrl=${theUrl}`); - // TODO: Obsidian doesn't support ArrayBuffer - // currently downgraded to fetch()! - await fetch(theUrl, { - method: "PUT", - body: payload, - headers: { - Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, - }, - }); + if (requireApiVersion(API_VER_REQURL)) { + await requestUrl({ + url: theUrl, + method: "PUT", + body: payload, + headers: { + Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, + }, + }); + } else { + await fetch(theUrl, { + method: "PUT", + body: payload, + headers: { + Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, + }, + }); + } }; /** @@ -527,7 +545,7 @@ export class WrappedOnedriveClient { rangeEnd - 1 }, len=${rangeEnd - rangeStart}, size=${size}` ); - // TODO: Obsidian doesn't support ArrayBuffer + // obsidian requestUrl doesn't support setting Content-Length // currently downgraded to fetch()! // AND, NO AUTH HEADER here! const res = await fetch(theUrl, { @@ -539,8 +557,7 @@ export class WrappedOnedriveClient { "Content-Type": "application/octet-stream", }, }); - - return res.json() as DriveItem | UploadSession; + return (await res.json()) as DriveItem | UploadSession; }; } @@ -704,40 +721,52 @@ export const uploadToRemote = async ( // no need to create parent folders firstly, cool! - // upload large files! - // ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online - - // 1. create uploadSession - // uploadFile already starts with /drive/special/approot:/${vaultName} - const s: UploadSession = await client.postJson( - `${uploadFile}:/createUploadSession`, - { - item: { - "@microsoft.graph.conflictBehavior": "replace", - }, - } - ); - const uploadUrl = s.uploadUrl; - log.debug("uploadSession = "); - log.debug(s); - - // 2. upload by ranges - // convert to uint8 - const uint8 = new Uint8Array(remoteContent); // hard code range size const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB const RANGE_SIZE = MIN_UNIT * 20; // about 6.5536 MB - // upload the ranges one by one - let rangeStart = 0; - while (rangeStart < uint8.byteLength) { - await client.putUint8ArrayByRange( - uploadUrl, - uint8, - rangeStart, - Math.min(rangeStart + RANGE_SIZE, uint8.byteLength), - uint8.byteLength + + if (remoteContent.byteLength <= RANGE_SIZE) { + // directly using put! + await client.putArrayBuffer( + `${uploadFile}:/content?${new URLSearchParams({ + "@microsoft.graph.conflictBehavior": "replace", + })}`, + remoteContent ); - rangeStart += RANGE_SIZE; + } else { + // upload large files! + // ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online + + // 1. create uploadSession + // uploadFile already starts with /drive/special/approot:/${vaultName} + const s: UploadSession = await client.postJson( + `${uploadFile}:/createUploadSession`, + { + item: { + "@microsoft.graph.conflictBehavior": "replace", + }, + } + ); + const uploadUrl = s.uploadUrl; + log.debug("uploadSession = "); + log.debug(s); + + // 2. upload by ranges + // convert to uint8 + const uint8 = new Uint8Array(remoteContent); + + // upload the ranges one by one + let rangeStart = 0; + while (rangeStart < uint8.byteLength) { + await client.putUint8ArrayByRange( + uploadUrl, + uint8, + rangeStart, + Math.min(rangeStart + RANGE_SIZE, uint8.byteLength), + uint8.byteLength + ); + rangeStart += RANGE_SIZE; + } } const res = await getRemoteMeta(client, uploadFile); @@ -755,8 +784,13 @@ const downloadFromRemoteRaw = async ( `${key}?$select=@microsoft.graph.downloadUrl` ); const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"]; - const content = await (await fetch(downloadUrl)).arrayBuffer(); - return content; + if (requireApiVersion(API_VER_REQURL)) { + const content = (await requestUrl({ url: downloadUrl })).arrayBuffer; + return content; + } else { + const content = await (await fetch(downloadUrl)).arrayBuffer(); + return content; + } }; export const downloadFromRemote = async ( diff --git a/src/remoteForS3.ts b/src/remoteForS3.ts index 09c4c63..f864289 100644 --- a/src/remoteForS3.ts +++ b/src/remoteForS3.ts @@ -11,11 +11,26 @@ import { S3Client, } from "@aws-sdk/client-s3"; import { Upload } from "@aws-sdk/lib-storage"; +import { HttpHandler, HttpRequest, HttpResponse } from "@aws-sdk/protocol-http"; +import { + FetchHttpHandler, + FetchHttpHandlerOptions, +} from "@aws-sdk/fetch-http-handler"; +// @ts-ignore +import { requestTimeout } from "@aws-sdk/fetch-http-handler/dist-es/request-timeout"; +import { buildQueryString } from "@aws-sdk/querystring-builder"; +import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types"; import { Buffer } from "buffer"; import * as mime from "mime-types"; -import { Vault } from "obsidian"; +import { + Vault, + requestUrl, + RequestUrlParam, + RequestUrlResponse, + requireApiVersion, +} from "obsidian"; import { Readable } from "stream"; -import { RemoteItem, S3Config } from "./baseTypes"; +import { API_VER_REQURL, RemoteItem, S3Config } from "./baseTypes"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { arrayBufferToBuffer, @@ -28,12 +43,117 @@ export { S3Client } from "@aws-sdk/client-s3"; import * as origLog from "loglevel"; const log = origLog.getLogger("rs-default"); +//////////////////////////////////////////////////////////////////////////////// +// special handler using Obsidian requestUrl +//////////////////////////////////////////////////////////////////////////////// + +/** + * This is close to origin implementation of FetchHttpHandler + * https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts + * that is released under Apache 2 License. + * But this uses Obsidian requestUrl instead. + */ +class ObsHttpHandler extends FetchHttpHandler { + requestTimeoutInMs: number; + constructor(options?: FetchHttpHandlerOptions) { + super(options); + this.requestTimeoutInMs = + options === undefined ? undefined : options.requestTimeout; + } + async handle( + request: HttpRequest, + { abortSignal }: HttpHandlerOptions = {} + ): Promise<{ response: HttpResponse }> { + if (abortSignal?.aborted) { + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + return Promise.reject(abortError); + } + + let path = request.path; + if (request.query) { + const queryString = buildQueryString(request.query); + if (queryString) { + path += `?${queryString}`; + } + } + + const { port, method } = request; + const url = `${request.protocol}//${request.hostname}${ + port ? `:${port}` : "" + }${path}`; + const body = + method === "GET" || method === "HEAD" ? undefined : request.body; + + const transformedHeaders = { ...request.headers }; + delete transformedHeaders["host"]; + delete transformedHeaders["Host"]; + delete transformedHeaders["content-length"]; + delete transformedHeaders["Content-Length"]; + + let contentType: string = undefined; + if (transformedHeaders["content-type"] !== undefined) { + contentType = transformedHeaders["content-type"]; + } + + let transformedBody: any = body; + if (ArrayBuffer.isView(body)) { + transformedBody = bufferToArrayBuffer(body); + } + + const param: RequestUrlParam = { + body: transformedBody, + headers: transformedHeaders, + method: method, + url: url, + contentType: contentType, + }; + + const raceOfPromises = [ + requestUrl(param).then((rsp) => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(rsp.arrayBuffer)); + controller.close(); + }, + }); + return { + response: new HttpResponse({ + headers: rsp.headers, + statusCode: rsp.status, + body: stream, + }), + }; + }), + requestTimeout(this.requestTimeoutInMs), + ]; + + if (abortSignal) { + raceOfPromises.push( + new Promise((resolve, reject) => { + abortSignal.onabort = () => { + const abortError = new Error("Request aborted"); + abortError.name = "AbortError"; + reject(abortError); + }; + }) + ); + } + return Promise.race(raceOfPromises); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// other stuffs +//////////////////////////////////////////////////////////////////////////////// + export const DEFAULT_S3_CONFIG = { s3Endpoint: "", s3Region: "", s3AccessKeyID: "", s3SecretAccessKey: "", s3BucketName: "", + bypassCorsLocally: true, }; export type S3ObjectType = _Object; @@ -66,15 +186,29 @@ export const getS3Client = (s3Config: S3Config) => { if (!(endpoint.startsWith("http://") || endpoint.startsWith("https://"))) { endpoint = `https://${endpoint}`; } - const s3Client = new S3Client({ - region: s3Config.s3Region, - endpoint: endpoint, - credentials: { - accessKeyId: s3Config.s3AccessKeyID, - secretAccessKey: s3Config.s3SecretAccessKey, - }, - }); - return s3Client; + + if (requireApiVersion(API_VER_REQURL) && s3Config.bypassCorsLocally) { + const s3Client = new S3Client({ + region: s3Config.s3Region, + endpoint: endpoint, + credentials: { + accessKeyId: s3Config.s3AccessKeyID, + secretAccessKey: s3Config.s3SecretAccessKey, + }, + requestHandler: new ObsHttpHandler(), + }); + return s3Client; + } else { + const s3Client = new S3Client({ + region: s3Config.s3Region, + endpoint: endpoint, + credentials: { + accessKeyId: s3Config.s3AccessKeyID, + secretAccessKey: s3Config.s3SecretAccessKey, + }, + }); + return s3Client; + } }; export const getRemoteMeta = async ( @@ -152,12 +286,13 @@ export const uploadToRemote = async ( if (password !== "") { remoteContent = await encryptArrayBuffer(localContent, password); } - const body = arrayBufferToBuffer(remoteContent); + const bytesIn5MB = 5242880; + const body = new Uint8Array(remoteContent); const upload = new Upload({ client: s3Client, queueSize: 20, // concurrency - partSize: 5242880, // minimal 5MB by default + partSize: bytesIn5MB, // minimal 5MB by default leavePartsOnError: false, params: { Bucket: s3Config.s3BucketName, diff --git a/src/remoteForWebdav.ts b/src/remoteForWebdav.ts index 8acdc1c..dbd6e95 100644 --- a/src/remoteForWebdav.ts +++ b/src/remoteForWebdav.ts @@ -1,18 +1,117 @@ import { Buffer } from "buffer"; -import { Vault } from "obsidian"; -import type { FileStat, WebDAVClient } from "webdav/web"; -import { AuthType, BufferLike, createClient } from "webdav/web"; +import { Vault, request, requestUrl, requireApiVersion } from "obsidian"; + import { Queue } from "@fyears/tsqueue"; import chunk from "lodash/chunk"; import flatten from "lodash/flatten"; -import type { RemoteItem, WebdavConfig } from "./baseTypes"; +import { getReasonPhrase } from "http-status-codes"; +import { API_VER_REQURL, RemoteItem, WebdavConfig } from "./baseTypes"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc"; -export type { WebDAVClient } from "webdav/web"; import * as origLog from "loglevel"; const log = origLog.getLogger("rs-default"); +import type { + FileStat, + WebDAVClient, + RequestOptionsWithState, + Response, + ResponseDataDetailed, +} from "webdav/web"; +import { getPatcher } from "webdav/web"; +if (requireApiVersion(API_VER_REQURL)) { + getPatcher().patch( + "request", + async ( + options: RequestOptionsWithState + ): Promise> => { + const transformedHeaders = { ...options.headers }; + delete transformedHeaders["host"]; + delete transformedHeaders["Host"]; + delete transformedHeaders["content-length"]; + delete transformedHeaders["Content-Length"]; + const r = await requestUrl({ + url: options.url, + method: options.method, + body: options.data as string | ArrayBuffer, + headers: transformedHeaders, + }); + + let r2: Response | ResponseDataDetailed = undefined; + if (options.responseType === undefined) { + r2 = { + data: undefined, + status: r.status, + statusText: getReasonPhrase(r.status), + headers: r.headers, + }; + } else if (options.responseType === "json") { + r2 = { + data: r.json, + status: r.status, + statusText: getReasonPhrase(r.status), + headers: r.headers, + }; + } else if (options.responseType === "text") { + r2 = { + data: r.text, + status: r.status, + statusText: getReasonPhrase(r.status), + headers: r.headers, + }; + } else if (options.responseType === "arraybuffer") { + r2 = { + data: r.arrayBuffer, + status: r.status, + statusText: getReasonPhrase(r.status), + headers: r.headers, + }; + } else { + throw Error( + `do not know how to deal with responseType = ${options.responseType}` + ); + } + return r2; + } + ); +} +// getPatcher().patch("request", (options: any) => { +// // console.log("using fetch"); +// const r = fetch(options.url, { +// method: options.method, +// body: options.data as any, +// headers: options.headers, +// signal: options.signal, +// }) +// .then((rsp) => { +// if (options.responseType === undefined) { +// return Promise.all([undefined, rsp]); +// } +// if (options.responseType === "json") { +// return Promise.all([rsp.json(), rsp]); +// } +// if (options.responseType === "text") { +// return Promise.all([rsp.text(), rsp]); +// } +// if (options.responseType === "arraybuffer") { +// return Promise.all([rsp.arrayBuffer(), rsp]); +// } +// }) +// .then(([d, r]) => { +// return { +// data: d, +// status: r.status, +// statusText: r.statusText, +// headers: r.headers, +// }; +// }); +// // console.log("using fetch"); +// return r; +// }); +import { AuthType, BufferLike, createClient } from "webdav/web"; +export type { WebDAVClient } from "webdav/web"; + export const DEFAULT_WEBDAV_CONFIG = { address: "", username: "", @@ -169,7 +268,7 @@ export const uploadToRemote = async ( // if encrypted, upload a fake file with the encrypted file name await client.client.putFileContents(uploadFile, "", { overwrite: true, - onUploadProgress: (progress) => { + onUploadProgress: (progress: any) => { // log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`); }, }); @@ -200,7 +299,7 @@ export const uploadToRemote = async ( } await client.client.putFileContents(uploadFile, remoteContent, { overwrite: true, - onUploadProgress: (progress) => { + onUploadProgress: (progress: any) => { log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`); }, }); diff --git a/src/settings.ts b/src/settings.ts index 7adab9a..ffccfc1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,8 +5,13 @@ import { PluginSettingTab, Setting, Platform, + requireApiVersion, } from "obsidian"; -import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes"; +import { + API_VER_REQURL, + SUPPORTED_SERVICES_TYPE, + WebdavAuthType, +} from "./baseTypes"; import { exportVaultSyncPlansToFiles } from "./debugMode"; import { exportQrCodeUri } from "./importExport"; import { @@ -595,9 +600,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab { cls: "s3-disclaimer", }); - s3Div.createEl("p", { - text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", - }); + if (!requireApiVersion(API_VER_REQURL)) { + s3Div.createEl("p", { + text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", + }); + } s3Div.createEl("p", { text: "Some Amazon S3 official docs for references:", @@ -615,10 +622,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab { text: "Access key ID and Secret access key info", }); - s3LinksUl.createEl("li").createEl("a", { - href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html", - text: "Configuring CORS", - }); + if (!requireApiVersion(API_VER_REQURL)) { + s3LinksUl.createEl("li").createEl("a", { + href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html", + text: "Configuring CORS", + }); + } new Setting(s3Div) .setName("s3Endpoint") @@ -687,6 +696,34 @@ export class RemotelySaveSettingTab extends PluginSettingTab { }) ); + if (requireApiVersion(API_VER_REQURL)) { + new Setting(s3Div) + .setName("bypass CORS issue locally") + .setDesc( + `The plugin allows skipping server CORS config in new version (Obsidian>=${API_VER_REQURL}). If you encounter any issues, please disable this setting and config CORS (app://obsidian.md and capacitor://localhost and http://localhost) on server.` + ) + .addDropdown((dropdown) => { + dropdown + .addOption("disable", "disable") + .addOption("enable", "enable"); + + dropdown + .setValue( + `${ + this.plugin.settings.s3.bypassCorsLocally ? "enable" : "disable" + }` + ) + .onChange(async (value) => { + if (value === "enable") { + this.plugin.settings.s3.bypassCorsLocally = true; + } else { + this.plugin.settings.s3.bypassCorsLocally = false; + } + await this.plugin.saveSettings(); + }); + }); + } + new Setting(s3Div) .setName("check connectivity") .setDesc("check connectivity") @@ -960,9 +997,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab { cls: "webdav-disclaimer", }); - webdavDiv.createEl("p", { - text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", - }); + if (!requireApiVersion(API_VER_REQURL)) { + webdavDiv.createEl("p", { + text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", + }); + } webdavDiv.createEl("p", { text: `We will create and sync inside the folder /${this.app.vault.getName()} on your server.`, @@ -1010,9 +1049,20 @@ export class RemotelySaveSettingTab extends PluginSettingTab { new Setting(webdavDiv) .setName("server auth type") .setDesc("If no password, this option would be ignored.") - .addDropdown((dropdown) => { + .addDropdown(async (dropdown) => { dropdown.addOption("basic", "basic"); - // dropdown.addOption("digest", "digest"); + if (requireApiVersion(API_VER_REQURL)) { + dropdown.addOption("digest", "digest"); + } + + // new version config, copied to old version, we need to reset it + if ( + !requireApiVersion(API_VER_REQURL) && + this.plugin.settings.webdav.authType !== "basic" + ) { + this.plugin.settings.webdav.authType = "basic"; + await this.plugin.saveSettings(); + } dropdown .setValue(this.plugin.settings.webdav.authType) @@ -1067,8 +1117,13 @@ export class RemotelySaveSettingTab extends PluginSettingTab { if (res) { new Notice("Great! The webdav server can be accessed."); } else { + let corsErrMsg = "/CORS"; + if (requireApiVersion(API_VER_REQURL)) { + corsErrMsg = ""; + } + new Notice( - "The webdav server cannot be reached (possible to be any of address/username/password/authtype/CORS errors)." + `The webdav server cannot be reached (possible to be any of address/username/password/authtype${corsErrMsg} errors).` ); } }); diff --git a/tests/misc.test.ts b/tests/misc.test.ts index 11caf76..ce528e6 100644 --- a/tests/misc.test.ts +++ b/tests/misc.test.ts @@ -180,3 +180,56 @@ describe("Misc: extract svg", () => { expect(y).to.equal(""); }); }); + +describe("Misc: get split ranges", () => { + it("should deal with big parts", () => { + const k = misc.getSplitRanges(10, 20); + const k2: misc.SplitRange[] = [ + { + partNum: 1, + start: 0, + end: 10, + }, + ]; + expect(k).to.deep.equal(k2); + }); + + it("should deal with 0 remainder", () => { + const k = misc.getSplitRanges(20, 10); + const k2: misc.SplitRange[] = [ + { + partNum: 1, + start: 0, + end: 10, + }, + { + partNum: 2, + start: 10, + end: 20, + }, + ]; + expect(k).to.deep.equal(k2); + }); + + it("should deal with not-0 remainder", () => { + const k = misc.getSplitRanges(25, 10); + const k2: misc.SplitRange[] = [ + { + partNum: 1, + start: 0, + end: 10, + }, + { + partNum: 2, + start: 10, + end: 20, + }, + { + partNum: 3, + start: 20, + end: 25, + }, + ]; + expect(k).to.deep.equal(k2); + }); +});