remotely-save/src/fsWebdis.ts

266 lines
7.8 KiB
TypeScript

import { isEqual } from "lodash";
import {
DEFAULT_CONTENT_TYPE,
type Entity,
type WebdisConfig,
} from "./baseTypes";
import { FakeFs } from "./fsAll";
export const DEFAULT_WEBDIS_CONFIG: WebdisConfig = {
address: "",
username: "",
password: "",
remoteBaseDir: "",
};
const getWebdisPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `${remoteBaseDir}`;
} else if (fileOrFolderPath.startsWith("/")) {
console.warn(
`why the path ${fileOrFolderPath} starts with '/'? but we just go on.`
);
key = `${remoteBaseDir}${fileOrFolderPath}`;
} else {
key = `${remoteBaseDir}/${fileOrFolderPath}`;
}
return `rs:fs:v1:${encodeURIComponent(key)}`; // we should encode them!!!!
};
export const getOrigPath = (fullKey: string, remoteBaseDir: string) => {
const fullKeyDecoded = decodeURIComponent(fullKey);
const prefix = `rs:fs:v1:${remoteBaseDir}/`;
// console.debug(`prefix=${prefix}`);
const suffix1 = ":meta";
const suffix2 = ":content";
if (!fullKeyDecoded.startsWith(prefix)) {
throw Error(`you should not call getOrigEntity on ${fullKey}`);
}
let realKey = fullKeyDecoded.slice(prefix.length);
// console.debug(`realKey=${realKey}`);
if (realKey.endsWith(suffix1)) {
realKey = realKey.slice(0, -suffix1.length);
// console.debug(`realKey=${realKey}`);
} else if (realKey.endsWith(suffix2)) {
realKey = realKey.slice(0, -suffix2.length);
// console.debug(`realKey=${realKey}`);
}
// console.debug(`fullKey=${fullKey}, realKey=${realKey}`);
return realKey;
};
export class FakeFsWebdis extends FakeFs {
kind: "webdis";
webdisConfig: WebdisConfig;
remoteBaseDir: string;
saveUpdatedConfigFunc: () => Promise<any>;
constructor(
webdisConfig: WebdisConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
super();
this.kind = "webdis";
this.webdisConfig = webdisConfig;
this.remoteBaseDir = this.webdisConfig.remoteBaseDir || vaultName || "";
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
}
async _fetchCommand(
method: "GET" | "POST" | "PUT",
urlPath: string,
content?: ArrayBuffer
) {
const address = this.webdisConfig.address;
if (!address.startsWith(`https://`) && !address.startsWith(`http://`)) {
throw Error(
`your webdis server address should start with https:// or http://`
);
}
if (address.endsWith("/")) {
throw Error(`your webdis server should not ends with /`);
}
if (content !== undefined && method !== "PUT") {
throw Error(`you can only "POST" ArrayBuffer, not using other methods`);
}
const fullUrl = `${address}/${urlPath}`;
// console.debug(`fullUrl=${fullUrl}`)
const username = this.webdisConfig.username ?? "";
const password = this.webdisConfig.password ?? "";
if (username !== "" && password !== "") {
return await fetch(fullUrl, {
method: method,
headers: {
Authorization: "Basic " + btoa(username + ":" + password),
},
body: content,
});
} else if (username === "" && password === "") {
return await fetch(fullUrl, {
method: method,
body: content,
});
} else {
throw Error(
`your username and password should both be empty or not empty!`
);
}
}
async walk(): Promise<Entity[]> {
let cursor = "0";
const res: Entity[] = [];
do {
const command = `SCAN/${cursor}/MATCH/rs:fs:v1:*:meta/COUNT/1000`;
const rsp = (await (await this._fetchCommand("GET", command)).json())[
"SCAN"
];
// console.debug(rsp);
cursor = rsp[0];
for (const fullKeyWithMeta of rsp[1]) {
const realKey = getOrigPath(fullKeyWithMeta, this.remoteBaseDir);
res.push(await this.stat(realKey));
}
} while (cursor !== "0");
// console.debug(`walk res:`);
// console.debug(res);
return res;
}
async walkPartial(): Promise<Entity[]> {
let cursor = "0";
const res: Entity[] = [];
const command = `SCAN/${cursor}/MATCH/rs:fs:v1:*:meta/COUNT/10`; // fewer keys
const rsp = (await (await this._fetchCommand("GET", command)).json())[
"SCAN"
];
// console.debug(rsp);
cursor = rsp[0];
for (const fullKeyWithMeta of rsp[1]) {
const realKey = getOrigPath(fullKeyWithMeta, this.remoteBaseDir);
res.push(await this.stat(realKey));
}
// no need to loop over cursor
// console.debug(`walk res:`);
// console.debug(res);
return res;
}
async stat(key: string): Promise<Entity> {
const fullKey = getWebdisPath(key, this.remoteBaseDir);
return await this._statFromRaw(fullKey);
}
async _statFromRaw(key: string): Promise<Entity> {
// console.debug(`_statFromRaw on ${key}`);
const command = `HGETALL/${key}:meta`;
const rsp = (await (await this._fetchCommand("GET", command)).json())[
"HGETALL"
];
// console.debug(`rsp: ${JSON.stringify(rsp, null, 2)}`);
if (isEqual(rsp, {})) {
// empty!
throw Error(`key ${key} doesn't exist!`);
}
const realKey = getOrigPath(key, this.remoteBaseDir);
return {
key: realKey,
keyRaw: realKey,
mtimeCli: Number.parseInt(rsp["mtime"]),
mtimeSvr: Number.parseInt(rsp["mtime"]),
size: Number.parseInt(rsp["size"]),
sizeRaw: Number.parseInt(rsp["size"]),
};
}
async mkdir(key: string, mtime?: number, ctime?: number): Promise<Entity> {
let command = `HSET/${getWebdisPath(key, this.remoteBaseDir)}:meta/size/0`;
if (mtime !== undefined && mtime !== 0) {
command = `${command}/mtime/${mtime}`;
}
if (ctime !== undefined && ctime !== 0) {
command = `${command}/ctime/${ctime}`;
}
const rsp = (await (await this._fetchCommand("GET", command)).json())[
"HSET"
];
return await this.stat(key);
}
async writeFile(
key: string,
content: ArrayBuffer,
mtime: number,
ctime: number
): Promise<Entity> {
const fullKey = getWebdisPath(key, this.remoteBaseDir);
// meta
let command1 = `HSET/${fullKey}:meta/size/${content.byteLength}`;
if (mtime !== undefined && mtime !== 0) {
command1 = `${command1}/mtime/${mtime}`;
}
if (ctime !== undefined && ctime !== 0) {
command1 = `${command1}/ctime/${ctime}`;
}
const rsp1 = (await (await this._fetchCommand("GET", command1)).json())[
"HSET"
];
// content
const command2 = `SET/${fullKey}:content`;
const rsp2 = (
await (await this._fetchCommand("PUT", command2, content)).json()
)["SET"];
// fetch meta
return await this.stat(key);
}
async readFile(key: string): Promise<ArrayBuffer> {
const fullKey = getWebdisPath(key, this.remoteBaseDir);
const command = `GET/${fullKey}:content?type=${DEFAULT_CONTENT_TYPE}`;
const rsp = await (await this._fetchCommand("GET", command)).arrayBuffer();
return rsp;
}
async rm(key: string): Promise<void> {
const fullKey = getWebdisPath(key, this.remoteBaseDir);
const command = `DEL/${fullKey}:meta/${fullKey}:content`;
const rsp = (await (await this._fetchCommand("PUT", command)).json())[
"DEL"
];
}
async checkConnect(callbackFunc?: any): Promise<boolean> {
try {
const k = await (
await this._fetchCommand("GET", "PING/helloworld")
).json();
return isEqual(k, { PING: "helloworld" });
} catch (err: any) {
console.error(err);
callbackFunc?.(err);
return false;
}
}
async getUserDisplayName(): Promise<string> {
return this.webdisConfig.username || "<no usernme>";
}
async revokeAuth(): Promise<any> {
throw new Error("Method not implemented.");
}
allowEmptyFile(): boolean {
return true;
}
}