...
This commit is contained in:
239
static/js/webdav-client.js
Normal file
239
static/js/webdav-client.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user