266 lines
7.7 KiB
JavaScript
266 lines
7.7 KiB
JavaScript
/**
|
|
* WebDAV Client
|
|
* Handles all WebDAV protocol operations
|
|
*/
|
|
|
|
class WebDAVClient {
|
|
constructor(baseUrl) {
|
|
this.baseUrl = baseUrl;
|
|
this.currentCollection = null;
|
|
}
|
|
|
|
setCollection(collection) {
|
|
this.currentCollection = collection;
|
|
}
|
|
|
|
getFullUrl(path) {
|
|
if (!this.currentCollection) {
|
|
throw new Error('No collection selected');
|
|
}
|
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
|
return `${this.baseUrl}${this.currentCollection}/${cleanPath}`;
|
|
}
|
|
|
|
async getCollections() {
|
|
const response = await fetch(this.baseUrl);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to get collections');
|
|
}
|
|
return await response.json();
|
|
}
|
|
|
|
async propfind(path = '', depth = '1') {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url, {
|
|
method: 'PROPFIND',
|
|
headers: {
|
|
'Depth': depth,
|
|
'Content-Type': 'application/xml'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`PROPFIND failed: ${response.statusText}`);
|
|
}
|
|
|
|
const xml = await response.text();
|
|
return this.parseMultiStatus(xml);
|
|
}
|
|
|
|
async get(path) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GET failed: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.text();
|
|
}
|
|
|
|
async getBinary(path) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GET failed: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.blob();
|
|
}
|
|
|
|
async put(path, content) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'text/plain'
|
|
},
|
|
body: content
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`PUT failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async putBinary(path, content) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url, {
|
|
method: 'PUT',
|
|
body: content
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`PUT failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async delete(path) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`DELETE failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async copy(sourcePath, destPath) {
|
|
const sourceUrl = this.getFullUrl(sourcePath);
|
|
const destUrl = this.getFullUrl(destPath);
|
|
|
|
const response = await fetch(sourceUrl, {
|
|
method: 'COPY',
|
|
headers: {
|
|
'Destination': destUrl
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`COPY failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async move(sourcePath, destPath) {
|
|
const sourceUrl = this.getFullUrl(sourcePath);
|
|
const destUrl = this.getFullUrl(destPath);
|
|
|
|
const response = await fetch(sourceUrl, {
|
|
method: 'MOVE',
|
|
headers: {
|
|
'Destination': destUrl
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`MOVE failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async mkcol(path) {
|
|
const url = this.getFullUrl(path);
|
|
const response = await fetch(url, {
|
|
method: 'MKCOL'
|
|
});
|
|
|
|
if (!response.ok && response.status !== 405) { // 405 means already exists
|
|
throw new Error(`MKCOL failed: ${response.statusText}`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async includeFile(path) {
|
|
try {
|
|
// Parse path: "collection:path/to/file" or "path/to/file"
|
|
let targetCollection = this.currentCollection;
|
|
let targetPath = path;
|
|
|
|
if (path.includes(':')) {
|
|
[targetCollection, targetPath] = path.split(':');
|
|
}
|
|
|
|
// Temporarily switch collection
|
|
const originalCollection = this.currentCollection;
|
|
this.currentCollection = targetCollection;
|
|
|
|
const content = await this.get(targetPath);
|
|
|
|
// Restore collection
|
|
this.currentCollection = originalCollection;
|
|
|
|
return content;
|
|
} catch (error) {
|
|
throw new Error(`Cannot include file "${path}": ${error.message}`);
|
|
}
|
|
}
|
|
|
|
parseMultiStatus(xml) {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(xml, 'text/xml');
|
|
const responses = doc.getElementsByTagNameNS('DAV:', 'response');
|
|
|
|
const items = [];
|
|
for (let i = 0; i < responses.length; i++) {
|
|
const response = responses[i];
|
|
const href = response.getElementsByTagNameNS('DAV:', 'href')[0].textContent;
|
|
const propstat = response.getElementsByTagNameNS('DAV:', 'propstat')[0];
|
|
const prop = propstat.getElementsByTagNameNS('DAV:', 'prop')[0];
|
|
|
|
// Check if it's a collection (directory)
|
|
const resourcetype = prop.getElementsByTagNameNS('DAV:', 'resourcetype')[0];
|
|
const isDirectory = resourcetype.getElementsByTagNameNS('DAV:', 'collection').length > 0;
|
|
|
|
// Get size
|
|
const contentlengthEl = prop.getElementsByTagNameNS('DAV:', 'getcontentlength')[0];
|
|
const size = contentlengthEl ? parseInt(contentlengthEl.textContent) : 0;
|
|
|
|
// Extract path relative to collection
|
|
const pathParts = href.split(`/${this.currentCollection}/`);
|
|
const relativePath = pathParts.length > 1 ? pathParts[1] : '';
|
|
|
|
// Skip the collection root itself
|
|
if (!relativePath) continue;
|
|
|
|
// Remove trailing slash from directories
|
|
const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
|
|
|
|
items.push({
|
|
path: cleanPath,
|
|
name: cleanPath.split('/').pop(),
|
|
isDirectory,
|
|
size
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
buildTree(items) {
|
|
const root = [];
|
|
const map = {};
|
|
|
|
// Sort items by path depth and name
|
|
items.sort((a, b) => {
|
|
const depthA = a.path.split('/').length;
|
|
const depthB = b.path.split('/').length;
|
|
if (depthA !== depthB) return depthA - depthB;
|
|
return a.path.localeCompare(b.path);
|
|
});
|
|
|
|
items.forEach(item => {
|
|
const parts = item.path.split('/');
|
|
const parentPath = parts.slice(0, -1).join('/');
|
|
|
|
const node = {
|
|
...item,
|
|
children: []
|
|
};
|
|
|
|
map[item.path] = node;
|
|
|
|
if (parentPath && map[parentPath]) {
|
|
map[parentPath].children.push(node);
|
|
} else {
|
|
root.push(node);
|
|
}
|
|
|
|
});
|
|
|
|
return root;
|
|
}
|
|
}
|
|
|