mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 14:02:08 +08:00
feat: add webdav support
This commit is contained in:
33
app/utils/cloud/index.ts
Normal file
33
app/utils/cloud/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createWebDavClient } from "./webdav";
|
||||
import { createUpstashClient } from "./upstash";
|
||||
|
||||
export enum ProviderType {
|
||||
WebDAV = "webdav",
|
||||
UpStash = "upstash",
|
||||
}
|
||||
|
||||
export const SyncClients = {
|
||||
[ProviderType.UpStash]: createUpstashClient,
|
||||
[ProviderType.WebDAV]: createWebDavClient,
|
||||
} as const;
|
||||
|
||||
type SyncClientConfig = {
|
||||
[K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends (
|
||||
_: infer C,
|
||||
) => any
|
||||
? C
|
||||
: never;
|
||||
};
|
||||
|
||||
export type SyncClient = {
|
||||
get: (key: string) => Promise<string>;
|
||||
set: (key: string, value: string) => Promise<void>;
|
||||
check: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
export function createSyncClient<T extends ProviderType>(
|
||||
provider: T,
|
||||
config: SyncClientConfig[T],
|
||||
): SyncClient {
|
||||
return SyncClients[provider](config as any) as any;
|
||||
}
|
39
app/utils/cloud/upstash.ts
Normal file
39
app/utils/cloud/upstash.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
|
||||
export type UpstashConfig = SyncStore["upstash"];
|
||||
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
||||
|
||||
export function createUpstashClient(config: UpstashConfig) {
|
||||
return {
|
||||
async check() {
|
||||
return true;
|
||||
},
|
||||
|
||||
async get() {
|
||||
throw Error("[Sync] not implemented");
|
||||
},
|
||||
|
||||
async set() {
|
||||
throw Error("[Sync] not implemented");
|
||||
},
|
||||
|
||||
headers() {
|
||||
return {
|
||||
Authorization: `Basic ${config.apiKey}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
},
|
||||
};
|
||||
}
|
78
app/utils/cloud/webdav.ts
Normal file
78
app/utils/cloud/webdav.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { STORAGE_KEY } from "@/app/constant";
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
import { corsFetch } from "../cors";
|
||||
|
||||
export type WebDAVConfig = SyncStore["webdav"];
|
||||
export type WebDavClient = ReturnType<typeof createWebDavClient>;
|
||||
|
||||
export function createWebDavClient(store: SyncStore) {
|
||||
const folder = STORAGE_KEY;
|
||||
const fileName = `${folder}/backup.json`;
|
||||
const config = store.webdav;
|
||||
const proxyUrl =
|
||||
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
|
||||
|
||||
return {
|
||||
async check() {
|
||||
try {
|
||||
const res = await corsFetch(this.path(folder), {
|
||||
method: "MKCOL",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] check", res.status, res.statusText);
|
||||
|
||||
return [201, 200, 404].includes(res.status);
|
||||
} catch (e) {
|
||||
console.error("[WebDav] failed to check", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
async get(key: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
||||
|
||||
return await res.text();
|
||||
},
|
||||
|
||||
async set(key: string, value: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
method: "PUT",
|
||||
headers: this.headers(),
|
||||
body: value,
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] set key = ", key, res.status, res.statusText);
|
||||
},
|
||||
|
||||
headers() {
|
||||
const auth = btoa(config.username + ":" + config.password);
|
||||
|
||||
return {
|
||||
authorization: `Basic ${auth}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
},
|
||||
};
|
||||
}
|
50
app/utils/cors.ts
Normal file
50
app/utils/cors.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ApiPath, DEFAULT_CORS_HOST } from "../constant";
|
||||
|
||||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : "";
|
||||
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
export function corsFetch(
|
||||
url: string,
|
||||
options: RequestInit & {
|
||||
proxyUrl?: string;
|
||||
},
|
||||
) {
|
||||
if (!url.startsWith("http")) {
|
||||
throw Error("[CORS Fetch] url must starts with http/https");
|
||||
}
|
||||
|
||||
let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors);
|
||||
if (!proxyUrl.endsWith("/")) {
|
||||
proxyUrl += "/";
|
||||
}
|
||||
|
||||
url = url.replace("://", "/");
|
||||
|
||||
const corsOptions = {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: options.method
|
||||
? {
|
||||
...options.headers,
|
||||
method: options.method,
|
||||
}
|
||||
: options.headers,
|
||||
};
|
||||
|
||||
const corsUrl = proxyUrl + url;
|
||||
console.info("[CORS] target = ", corsUrl);
|
||||
|
||||
return fetch(corsUrl, corsOptions);
|
||||
}
|
Reference in New Issue
Block a user