240 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			240 lines
		
	
	
		
			6.8 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;
 | 
						|
    }
 | 
						|
    
 | 
						|
    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;
 | 
						|
    }
 | 
						|
}
 | 
						|
 |