mirror of
https://github.com/katelya77/K-Vault.git
synced 2026-05-06 22:10:57 +08:00
370 lines
10 KiB
JavaScript
370 lines
10 KiB
JavaScript
function normalizeBaseUrl(raw) {
|
|
if (!raw) return '';
|
|
try {
|
|
return new URL(String(raw)).toString().replace(/\/+$/, '');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function normalizeToken(value) {
|
|
if (!value) return '';
|
|
return String(value).replace(/^Bearer\s+/i, '').trim();
|
|
}
|
|
|
|
function normalizePath(value) {
|
|
const normalized = String(value || '')
|
|
.replace(/\\/g, '/')
|
|
.trim();
|
|
|
|
const output = [];
|
|
for (const part of normalized.split('/')) {
|
|
const piece = part.trim();
|
|
if (!piece || piece === '.') continue;
|
|
if (piece === '..') {
|
|
output.pop();
|
|
continue;
|
|
}
|
|
output.push(piece);
|
|
}
|
|
return output.join('/');
|
|
}
|
|
|
|
function splitPath(value) {
|
|
const normalized = normalizePath(value);
|
|
if (!normalized) return [];
|
|
return normalized.split('/').filter(Boolean);
|
|
}
|
|
|
|
function encodeSegments(segments) {
|
|
if (!segments.length) return '';
|
|
return segments.map((segment) => encodeURIComponent(segment)).join('/');
|
|
}
|
|
|
|
function getRootUrlCandidates(config) {
|
|
const base = config.baseUrl || '';
|
|
if (!base) return [];
|
|
return Array.from(new Set([base, `${base}/`]));
|
|
}
|
|
|
|
function authMode(config) {
|
|
if (config.bearerToken) return 'bearer';
|
|
if (config.username && config.password) return 'basic';
|
|
return 'none';
|
|
}
|
|
|
|
function hasBasicAuth(config) {
|
|
return Boolean(config.username && config.password);
|
|
}
|
|
|
|
function hasBearerAuth(config) {
|
|
return Boolean(config.bearerToken);
|
|
}
|
|
|
|
export function getWebDAVConfig(env = {}) {
|
|
return {
|
|
baseUrl: normalizeBaseUrl(env.WEBDAV_BASE_URL),
|
|
username: String(env.WEBDAV_USERNAME || '').trim(),
|
|
password: String(env.WEBDAV_PASSWORD || ''),
|
|
bearerToken: normalizeToken(env.WEBDAV_BEARER_TOKEN || env.WEBDAV_TOKEN || ''),
|
|
rootPath: normalizePath(env.WEBDAV_ROOT_PATH || ''),
|
|
};
|
|
}
|
|
|
|
export function hasWebDAVConfig(env = {}) {
|
|
const config = getWebDAVConfig(env);
|
|
return Boolean(config.baseUrl) && authMode(config) !== 'none';
|
|
}
|
|
|
|
function buildAuthHeaders(config, extra = {}) {
|
|
const headers = { ...extra };
|
|
const mode = authMode(config);
|
|
if (mode === 'bearer') {
|
|
headers.Authorization = `Bearer ${config.bearerToken}`;
|
|
} else if (mode === 'basic') {
|
|
const encoded = btoa(`${config.username}:${config.password}`);
|
|
headers.Authorization = `Basic ${encoded}`;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
function buildStoragePath(config, storagePath = '') {
|
|
const allSegments = [...splitPath(config.rootPath), ...splitPath(storagePath)];
|
|
return encodeSegments(allSegments);
|
|
}
|
|
|
|
function buildUrl(config, storagePath = '') {
|
|
const relative = buildStoragePath(config, storagePath);
|
|
return relative ? `${config.baseUrl}/${relative}` : config.baseUrl;
|
|
}
|
|
|
|
async function decodeErrorTextSafe(response) {
|
|
try {
|
|
const text = await response.text();
|
|
return String(text || '').slice(0, 500);
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
async function fetchDav(config, method, storagePath = '', { headers = {}, body = null } = {}) {
|
|
return fetch(buildUrl(config, storagePath), {
|
|
method,
|
|
headers: buildAuthHeaders(config, headers),
|
|
body,
|
|
});
|
|
}
|
|
|
|
function buildAuthOverrideHeaders(config, mode, extraHeaders = {}) {
|
|
const headers = { ...extraHeaders };
|
|
if (mode === 'bearer' && hasBearerAuth(config)) {
|
|
headers.Authorization = `Bearer ${config.bearerToken}`;
|
|
return headers;
|
|
}
|
|
if (mode === 'basic' && hasBasicAuth(config)) {
|
|
const encoded = btoa(`${config.username}:${config.password}`);
|
|
headers.Authorization = `Basic ${encoded}`;
|
|
return headers;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(headers, 'Authorization')) {
|
|
delete headers.Authorization;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
async function fetchDavWithAuthMode(config, method, storagePath = '', mode = 'none', { headers = {}, body = null } = {}) {
|
|
return fetch(buildUrl(config, storagePath), {
|
|
method,
|
|
headers: buildAuthOverrideHeaders(config, mode, headers),
|
|
body,
|
|
});
|
|
}
|
|
|
|
function buildAuthAttemptModes(config) {
|
|
const attemptedModes = [];
|
|
const pushMode = (mode) => {
|
|
if (!mode || attemptedModes.includes(mode)) return;
|
|
attemptedModes.push(mode);
|
|
};
|
|
|
|
pushMode(authMode(config));
|
|
if (hasBearerAuth(config)) pushMode('bearer');
|
|
if (hasBasicAuth(config)) pushMode('basic');
|
|
pushMode('none');
|
|
|
|
return attemptedModes;
|
|
}
|
|
|
|
async function ensureCollectionPath(config, storagePath) {
|
|
const rootSegments = splitPath(config.rootPath);
|
|
const fileSegments = splitPath(storagePath);
|
|
const directorySegments = [...rootSegments, ...fileSegments.slice(0, -1)];
|
|
if (directorySegments.length === 0) return;
|
|
|
|
for (let index = 0; index < directorySegments.length; index += 1) {
|
|
const partial = directorySegments.slice(0, index + 1);
|
|
const encoded = encodeSegments(partial);
|
|
const candidates = Array.from(new Set([
|
|
`${config.baseUrl}/${encoded}`,
|
|
`${config.baseUrl}/${encoded}/`,
|
|
]));
|
|
|
|
let success = false;
|
|
let lastResponse = null;
|
|
for (const url of candidates) {
|
|
const response = await fetch(url, {
|
|
method: 'MKCOL',
|
|
headers: buildAuthHeaders(config),
|
|
});
|
|
if ([200, 201, 204, 301, 302, 405].includes(response.status)) {
|
|
success = true;
|
|
break;
|
|
}
|
|
lastResponse = response;
|
|
}
|
|
|
|
if (!success) {
|
|
const detail = lastResponse ? await decodeErrorTextSafe(lastResponse) : '';
|
|
const statusCode = lastResponse ? lastResponse.status : 'N/A';
|
|
throw new Error(`WebDAV MKCOL failed (${statusCode}): ${detail || 'Unknown error'}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function uploadToWebDAV(arrayBuffer, storagePath, contentType, env = {}) {
|
|
const config = getWebDAVConfig(env);
|
|
if (!config.baseUrl) {
|
|
throw new Error('WebDAV base URL is not configured.');
|
|
}
|
|
if (authMode(config) === 'none') {
|
|
throw new Error('WebDAV auth is not configured.');
|
|
}
|
|
|
|
await ensureCollectionPath(config, storagePath);
|
|
|
|
const response = await fetchDav(config, 'PUT', storagePath, {
|
|
headers: {
|
|
'Content-Type': contentType || 'application/octet-stream',
|
|
'Content-Length': String(arrayBuffer.byteLength || 0),
|
|
},
|
|
body: arrayBuffer,
|
|
});
|
|
|
|
if (!response.ok && ![201, 204].includes(response.status)) {
|
|
const detail = await decodeErrorTextSafe(response);
|
|
throw new Error(`WebDAV upload failed (${response.status}): ${detail || 'Unknown error'}`);
|
|
}
|
|
|
|
return {
|
|
path: normalizePath(storagePath),
|
|
etag: response.headers.get('etag') || null,
|
|
};
|
|
}
|
|
|
|
export async function getWebDAVFile(storagePath, env = {}, options = {}) {
|
|
const config = getWebDAVConfig(env);
|
|
if (!config.baseUrl) {
|
|
throw new Error('WebDAV base URL is not configured.');
|
|
}
|
|
if (authMode(config) === 'none') {
|
|
throw new Error('WebDAV auth is not configured.');
|
|
}
|
|
|
|
const headers = {};
|
|
if (options.range) {
|
|
headers.Range = options.range;
|
|
}
|
|
|
|
const attemptedModes = buildAuthAttemptModes(config);
|
|
|
|
let firstFailure = null;
|
|
|
|
for (const mode of attemptedModes) {
|
|
const response = await fetchDavWithAuthMode(config, 'GET', storagePath, mode, { headers });
|
|
if (response.ok || response.status === 206) {
|
|
return response;
|
|
}
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
|
|
if (!firstFailure) {
|
|
const detail = await decodeErrorTextSafe(response);
|
|
firstFailure = { status: response.status, detail };
|
|
}
|
|
|
|
if (![400, 401, 403].includes(response.status)) {
|
|
const detail = await decodeErrorTextSafe(response);
|
|
throw new Error(`WebDAV download failed (${response.status}): ${detail || 'Unknown error'}`);
|
|
}
|
|
}
|
|
|
|
if (firstFailure) {
|
|
throw new Error(`WebDAV download failed (${firstFailure.status}): ${firstFailure.detail || 'Unknown error'}`);
|
|
}
|
|
|
|
throw new Error('WebDAV download failed: Unknown error');
|
|
}
|
|
|
|
export async function deleteWebDAVFile(storagePath, env = {}) {
|
|
const config = getWebDAVConfig(env);
|
|
if (!config.baseUrl) return false;
|
|
if (authMode(config) === 'none') return false;
|
|
|
|
const response = await fetchDav(config, 'DELETE', storagePath);
|
|
if (response.ok || response.status === 404) return true;
|
|
|
|
const detail = await decodeErrorTextSafe(response);
|
|
throw new Error(`WebDAV delete failed (${response.status}): ${detail || 'Unknown error'}`);
|
|
}
|
|
|
|
export async function checkWebDAVConnection(env = {}) {
|
|
if (!hasWebDAVConfig(env)) {
|
|
return {
|
|
connected: false,
|
|
configured: false,
|
|
message: 'Not configured',
|
|
};
|
|
}
|
|
|
|
const config = getWebDAVConfig(env);
|
|
try {
|
|
const rootCandidates = getRootUrlCandidates(config);
|
|
const propfindBody = [
|
|
'<?xml version="1.0" encoding="utf-8" ?>',
|
|
'<d:propfind xmlns:d="DAV:">',
|
|
' <d:prop><d:displayname /></d:prop>',
|
|
'</d:propfind>',
|
|
].join('');
|
|
|
|
let lastDetail = '';
|
|
let lastStatus = null;
|
|
|
|
const attemptedModes = buildAuthAttemptModes(config);
|
|
|
|
for (const mode of attemptedModes) {
|
|
for (const rootUrl of rootCandidates) {
|
|
const optionsResponse = await fetch(rootUrl, {
|
|
method: 'OPTIONS',
|
|
headers: buildAuthOverrideHeaders(config, mode, { Depth: '0' }),
|
|
});
|
|
|
|
if (optionsResponse.ok) {
|
|
return {
|
|
connected: true,
|
|
configured: true,
|
|
status: optionsResponse.status,
|
|
message: 'Connected',
|
|
};
|
|
}
|
|
|
|
if (![400, 401, 403, 405].includes(optionsResponse.status)) {
|
|
lastStatus = optionsResponse.status;
|
|
lastDetail = await decodeErrorTextSafe(optionsResponse);
|
|
}
|
|
|
|
const propfindResponse = await fetch(rootUrl, {
|
|
method: 'PROPFIND',
|
|
headers: buildAuthOverrideHeaders(config, mode, {
|
|
Depth: '0',
|
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
}),
|
|
body: propfindBody,
|
|
});
|
|
|
|
const connected = propfindResponse.ok || propfindResponse.status === 207;
|
|
if (connected) {
|
|
return {
|
|
connected: true,
|
|
configured: true,
|
|
status: propfindResponse.status,
|
|
message: 'Connected',
|
|
};
|
|
}
|
|
|
|
lastStatus = propfindResponse.status;
|
|
lastDetail = await decodeErrorTextSafe(propfindResponse);
|
|
}
|
|
}
|
|
|
|
return {
|
|
connected: false,
|
|
configured: true,
|
|
status: lastStatus || undefined,
|
|
message: lastDetail || 'Connection failed',
|
|
detail: lastDetail || undefined,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
connected: false,
|
|
configured: true,
|
|
message: error.message || 'Connection failed',
|
|
detail: error.message || 'Connection failed',
|
|
};
|
|
}
|
|
}
|
|
|
|
export function normalizeWebDAVPath(value = '') {
|
|
return normalizePath(value);
|
|
}
|