remotely-save/src/remoteForWebdav.ts
fyears c04876e0c5 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!
2022-03-10 23:54:35 +08:00

468 lines
13 KiB
TypeScript

import { Buffer } from "buffer";
import { Vault, request, requestUrl, requireApiVersion } from "obsidian";
import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk";
import flatten from "lodash/flatten";
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";
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: "",
password: "",
authType: "basic",
manualRecursive: false,
} as WebdavConfig;
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `/${vaultName}/`;
}
if (!fileOrFolderPath.startsWith("/")) {
key = `/${vaultName}/${fileOrFolderPath}`;
}
return key;
};
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
if (
!(
fileOrFolderPath === `/${vaultName}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`)
)
) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
}
// if (fileOrFolderPath.startsWith("/")) {
// return fileOrFolderPath.slice(1);
// }
return fileOrFolderPath.slice(`/${vaultName}/`.length);
};
const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
let key = getNormPath(x.filename, vaultName);
if (x.type === "directory" && !key.endsWith("/")) {
key = `${key}/`;
}
return {
key: key,
lastModified: Date.parse(x.lastmod).valueOf(),
size: x.size,
remoteType: "webdav",
etag: x.etag || undefined,
} as RemoteItem;
};
export class WrappedWebdavClient {
webdavConfig: WebdavConfig;
vaultName: string;
client: WebDAVClient;
vaultFolderExists: boolean;
constructor(webdavConfig: WebdavConfig, vaultName: string) {
this.webdavConfig = webdavConfig;
this.vaultName = vaultName;
this.vaultFolderExists = false;
}
init = async () => {
// init client if not inited
if (this.client === undefined) {
if (
this.webdavConfig.username !== "" &&
this.webdavConfig.password !== ""
) {
this.client = createClient(this.webdavConfig.address, {
username: this.webdavConfig.username,
password: this.webdavConfig.password,
authType:
this.webdavConfig.authType === "digest"
? AuthType.Digest
: AuthType.Password,
});
} else {
log.info("no password");
this.client = createClient(this.webdavConfig.address);
}
}
// check vault folder
if (this.vaultFolderExists) {
// pass
} else {
const res = await this.client.exists(`/${this.vaultName}`);
if (res) {
// log.info("remote vault folder exits!");
this.vaultFolderExists = true;
} else {
log.info("remote vault folder not exists, creating");
await this.client.createDirectory(`/${this.vaultName}`);
log.info("remote vault folder created!");
this.vaultFolderExists = true;
}
}
};
}
export const getWebdavClient = (
webdavConfig: WebdavConfig,
vaultName: string
) => {
return new WrappedWebdavClient(webdavConfig, vaultName);
};
export const getRemoteMeta = async (
client: WrappedWebdavClient,
fileOrFolderPath: string
) => {
await client.init();
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName);
// log.info(`remotePath = ${remotePath}`);
const res = (await client.client.stat(remotePath, {
details: false,
})) as FileStat;
return fromWebdavItemToRemoteItem(res, client.vaultName);
};
export const uploadToRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
vault: Vault,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = ""
) => {
await client.init();
let uploadFile = fileOrFolderPath;
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getWebdavPath(uploadFile, client.vaultName);
const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) {
throw Error("upload function doesn't implement recursive function yet!");
} else if (isFolder && !isRecursively) {
if (uploadRaw) {
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
if (password === "") {
// if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, {
recursive: true,
});
const res = await getRemoteMeta(client, uploadFile);
return res;
} else {
// if encrypted, upload a fake file with the encrypted file name
await client.client.putFileContents(uploadFile, "", {
overwrite: true,
onUploadProgress: (progress: any) => {
// log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
},
});
return await getRemoteMeta(client, uploadFile);
}
} else {
// file
// we ignore isRecursively parameter here
let localContent = undefined;
if (uploadRaw) {
if (typeof rawContent === "string") {
localContent = new TextEncoder().encode(rawContent).buffer;
} else {
localContent = rawContent;
}
} else {
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
}
// we need to create folders before uploading
const dir = getPathFolder(uploadFile);
if (dir !== "/" && dir !== "") {
await client.client.createDirectory(dir, { recursive: true });
}
await client.client.putFileContents(uploadFile, remoteContent, {
overwrite: true,
onUploadProgress: (progress: any) => {
log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
},
});
return await getRemoteMeta(client, uploadFile);
}
};
export const listFromRemote = async (
client: WrappedWebdavClient,
prefix?: string
) => {
if (prefix !== undefined) {
throw Error("prefix not supported");
}
await client.init();
let contents = [] as FileStat[];
if (client.webdavConfig.manualRecursive) {
// the remote doesn't support infinity propfind,
// we need to do a bfs here
const q = new Queue([`/${client.vaultName}`]);
const CHUNK_SIZE = 10;
while (q.length > 0) {
const itemsToFetch = [];
while (q.length > 0) {
itemsToFetch.push(q.pop());
}
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
// log.debug(itemsToFetchChunks);
const subContents = [] as FileStat[];
for (const singleChunk of itemsToFetchChunks) {
const r = singleChunk.map((x) => {
return client.client.getDirectoryContents(x, {
deep: false,
details: false /* no need for verbose details here */,
glob: "/**" /* avoid dot files by using glob */,
}) as Promise<FileStat[]>;
});
const r2 = flatten(await Promise.all(r));
subContents.push(...r2);
}
for (let i = 0; i < subContents.length; ++i) {
const f = subContents[i];
contents.push(f);
if (f.type === "directory") {
q.push(f.filename);
}
}
}
} else {
// the remote supports infinity propfind
contents = (await client.client.getDirectoryContents(
`/${client.vaultName}`,
{
deep: true,
details: false /* no need for verbose details here */,
glob: "/**" /* avoid dot files by using glob */,
}
)) as FileStat[];
}
return {
Contents: contents.map((x) =>
fromWebdavItemToRemoteItem(x, client.vaultName)
),
};
};
const downloadFromRemoteRaw = async (
client: WrappedWebdavClient,
fileOrFolderPath: string
) => {
await client.init();
const buff = (await client.client.getFileContents(
getWebdavPath(fileOrFolderPath, client.vaultName)
)) as BufferLike;
if (buff instanceof ArrayBuffer) {
return buff;
} else if (buff instanceof Buffer) {
return bufferToArrayBuffer(buff);
}
throw Error(`unexpected file content result with type ${typeof buff}`);
};
export const downloadFromRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
remoteEncryptedKey: string = "",
skipSaving: boolean = false
) => {
await client.init();
const isFolder = fileOrFolderPath.endsWith("/");
if (!skipSaving) {
await mkdirpInVault(fileOrFolderPath, vault);
}
// the file is always local file
// we need to encrypt it
if (isFolder) {
// mkdirp locally is enough
// do nothing here
return new ArrayBuffer(0);
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getWebdavPath(downloadFile, client.vaultName);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
localContent = await decryptArrayBuffer(remoteContent, password);
}
if (!skipSaving) {
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
mtime: mtime,
});
}
return localContent;
}
};
export const deleteFromRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
password: string = "",
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getWebdavPath(remoteFileName, client.vaultName);
await client.init();
try {
await client.client.deleteFile(remoteFileName);
// log.info(`delete ${remoteFileName} succeeded`);
} catch (err) {
console.error("some error while deleting");
log.info(err);
}
};
export const checkConnectivity = async (client: WrappedWebdavClient) => {
try {
await client.init();
const results = await getRemoteMeta(client, "/");
if (results === undefined) {
return false;
}
return true;
} catch (err) {
return false;
}
};