mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-17 06:03:44 +08:00
241 lines
7.2 KiB
JavaScript
241 lines
7.2 KiB
JavaScript
/**
|
|
* WebDAV API helper
|
|
*
|
|
* Uses only Fetch/Web APIs so it works in Cloudflare Pages Functions,
|
|
* Cloudflare Workers, and the local Node-based test/runtime paths.
|
|
*/
|
|
export class WebDAVAPI {
|
|
constructor(config = {}) {
|
|
const baseUrl = config.baseUrl;
|
|
if (!baseUrl) {
|
|
throw new Error('WebDAV baseUrl is required');
|
|
}
|
|
|
|
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
this.username = config.username || '';
|
|
this.password = config.password || '';
|
|
this.headers = normalizeHeaders(config.headers || {});
|
|
this.createDirectory = config.createDirectory !== false;
|
|
}
|
|
|
|
buildObjectUrl(path) {
|
|
return buildWebDAVUrl(this.baseUrl, path);
|
|
}
|
|
|
|
buildPublicUrl(path, publicUrl = '') {
|
|
if (!publicUrl) return '';
|
|
return buildWebDAVUrl(normalizeBaseUrl(publicUrl), path);
|
|
}
|
|
|
|
getRequestHeaders(extraHeaders = {}) {
|
|
const headers = new Headers(this.headers);
|
|
|
|
if ((this.username || this.password) && !headers.has('Authorization')) {
|
|
headers.set('Authorization', `Basic ${base64EncodeUtf8(`${this.username}:${this.password}`)}`);
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(extraHeaders || {})) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
headers.set(key, value);
|
|
}
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
async ensureDirectory(path) {
|
|
if (!this.createDirectory) return;
|
|
|
|
const dirParts = getDirectoryParts(path);
|
|
if (dirParts.length === 0) return;
|
|
|
|
let currentPath = '';
|
|
for (const part of dirParts) {
|
|
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
const response = await fetch(this.buildObjectUrl(currentPath), {
|
|
method: 'MKCOL',
|
|
headers: this.getRequestHeaders(),
|
|
redirect: 'manual',
|
|
});
|
|
|
|
// 405 commonly means the collection already exists. Some servers return 200/204.
|
|
if (![200, 201, 204, 405].includes(response.status)) {
|
|
throw new Error(`WebDAV MKCOL failed for ${currentPath}: ${response.status} ${response.statusText}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async putFile(path, body, contentType = '') {
|
|
await this.ensureDirectory(path);
|
|
|
|
const headers = this.getRequestHeaders(contentType ? { 'Content-Type': contentType } : {});
|
|
const response = await fetch(this.buildObjectUrl(path), {
|
|
method: 'PUT',
|
|
headers,
|
|
body,
|
|
redirect: 'manual',
|
|
});
|
|
|
|
if (!isSuccessStatus(response.status)) {
|
|
const detail = await safeReadResponseText(response);
|
|
throw new Error(`WebDAV PUT failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async getFile(path, options = {}) {
|
|
const response = await fetch(this.buildObjectUrl(path), {
|
|
method: options.method || 'GET',
|
|
headers: this.getRequestHeaders(options.headers || {}),
|
|
redirect: 'manual',
|
|
});
|
|
|
|
if (!isSuccessStatus(response.status) && response.status !== 304) {
|
|
const detail = await safeReadResponseText(response);
|
|
throw new Error(`WebDAV ${options.method || 'GET'} failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
async moveFile(oldPath, newPath, overwrite = true) {
|
|
await this.ensureDirectory(newPath);
|
|
|
|
const response = await fetch(this.buildObjectUrl(oldPath), {
|
|
method: 'MOVE',
|
|
headers: this.getRequestHeaders({
|
|
Destination: this.buildObjectUrl(newPath),
|
|
Overwrite: overwrite ? 'T' : 'F',
|
|
}),
|
|
redirect: 'manual',
|
|
});
|
|
|
|
if (!isSuccessStatus(response.status)) {
|
|
const detail = await safeReadResponseText(response);
|
|
throw new Error(`WebDAV MOVE failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async deleteFile(path) {
|
|
const response = await fetch(this.buildObjectUrl(path), {
|
|
method: 'DELETE',
|
|
headers: this.getRequestHeaders(),
|
|
redirect: 'manual',
|
|
});
|
|
|
|
// DELETE is idempotent for app semantics; a missing remote object should not block DB cleanup.
|
|
if (response.status === 404) return true;
|
|
|
|
if (!isSuccessStatus(response.status)) {
|
|
const detail = await safeReadResponseText(response);
|
|
throw new Error(`WebDAV DELETE failed: ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ''}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function normalizeBaseUrl(baseUrl) {
|
|
const normalized = String(baseUrl || '').trim();
|
|
if (!normalized) {
|
|
throw new Error('WebDAV baseUrl is required');
|
|
}
|
|
|
|
const url = new URL(normalized);
|
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
throw new Error('WebDAV baseUrl must use http or https');
|
|
}
|
|
|
|
if (!url.pathname.endsWith('/')) {
|
|
url.pathname = `${url.pathname}/`;
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
export function buildWebDAVUrl(baseUrl, path) {
|
|
const cleanPath = String(path || '')
|
|
.replace(/^\/+/, '')
|
|
.split('/')
|
|
.filter(Boolean)
|
|
.map(encodeURIComponent)
|
|
.join('/');
|
|
|
|
return new URL(cleanPath, normalizeBaseUrl(baseUrl)).toString();
|
|
}
|
|
|
|
export function normalizeWebDAVHeaders(headers) {
|
|
return normalizeHeaders(headers);
|
|
}
|
|
|
|
function getDirectoryParts(path) {
|
|
const parts = String(path || '').replace(/^\/+/, '').split('/').filter(Boolean);
|
|
parts.pop();
|
|
return parts;
|
|
}
|
|
|
|
function normalizeHeaders(headers) {
|
|
if (!headers) return {};
|
|
|
|
if (typeof headers === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(headers);
|
|
return normalizeHeaders(parsed);
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (headers instanceof Headers) {
|
|
const result = {};
|
|
headers.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
const result = {};
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
result[key] = String(value);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
function isSuccessStatus(status) {
|
|
return status >= 200 && status < 300;
|
|
}
|
|
|
|
async function safeReadResponseText(response) {
|
|
try {
|
|
const text = await response.text();
|
|
return text.slice(0, 500);
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function base64EncodeUtf8(value) {
|
|
const bytes = new TextEncoder().encode(value);
|
|
let binary = '';
|
|
const chunkSize = 0x8000;
|
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
}
|
|
|
|
if (typeof btoa === 'function') {
|
|
return btoa(binary);
|
|
}
|
|
|
|
// Node-based local tests/runtimes.
|
|
return Buffer.from(value, 'utf8').toString('base64');
|
|
}
|