This commit is contained in:
2025-10-26 07:17:49 +04:00
commit e41e49f7ea
23 changed files with 5070 additions and 0 deletions

239
static/js/webdav-client.js Normal file
View 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;
}
}