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!
This commit is contained in:
parent
5402baed0a
commit
c04876e0c5
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
32
src/misc.ts
32
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;
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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<Uint8Array>({
|
||||
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<never>((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,
|
||||
|
||||
@ -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<Response | ResponseDataDetailed<any>> => {
|
||||
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<any> = 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}`);
|
||||
},
|
||||
});
|
||||
|
||||
@ -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).`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -180,3 +180,56 @@ describe("Misc: extract svg", () => {
|
||||
expect(y).to.equal("<rect/><g/>");
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user