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;
 | |
|     }
 | |
| }
 | |
| 
 |