- Add API endpoint and handler to delete collections - Introduce LoadingSpinner component for async operations - Show loading spinners during file loading and preview rendering - Enhance modal accessibility by removing aria-hidden attribute - Refactor delete functionality to distinguish between collections and files/folders - Remove unused collection definitions from config
		
			
				
	
	
		
			604 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			604 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * File Tree Actions Manager
 | |
|  * Centralized handling of all tree operations
 | |
|  */
 | |
| 
 | |
| class FileTreeActions {
 | |
|     constructor(webdavClient, fileTree, editor) {
 | |
|         this.webdavClient = webdavClient;
 | |
|         this.fileTree = fileTree;
 | |
|         this.editor = editor;
 | |
|         this.clipboard = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Validate and sanitize filename/folder name
 | |
|      * Returns { valid: boolean, sanitized: string, message: string }
 | |
|      * Now uses ValidationUtils from utils.js
 | |
|      */
 | |
|     validateFileName(name, isFolder = false) {
 | |
|         return ValidationUtils.validateFileName(name, isFolder);
 | |
|     }
 | |
| 
 | |
|     async execute(action, targetPath, isDirectory) {
 | |
|         const handler = this.actions[action];
 | |
|         if (!handler) {
 | |
|             console.error(`Unknown action: ${action}`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|             await handler.call(this, targetPath, isDirectory);
 | |
|         } catch (error) {
 | |
|             console.error(`Action failed: ${action}`, error);
 | |
|             showNotification(`Failed to ${action}`, 'error');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     actions = {
 | |
|         open: async function (path, isDir) {
 | |
|             if (!isDir) {
 | |
|                 await this.editor.loadFile(path);
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         'new-file': async function (path, isDir) {
 | |
|             if (!isDir) return;
 | |
| 
 | |
|             const filename = await window.ModalManager.prompt(
 | |
|                 'Enter filename (lowercase, underscore only):',
 | |
|                 'new_file.md',
 | |
|                 'New File'
 | |
|             );
 | |
| 
 | |
|             if (!filename) return;
 | |
| 
 | |
|             let finalFilename = filename;
 | |
|             const validation = this.validateFileName(filename, false);
 | |
| 
 | |
|             if (!validation.valid) {
 | |
|                 showNotification(validation.message, 'warning');
 | |
| 
 | |
|                 // Ask if user wants to use sanitized version
 | |
|                 if (validation.sanitized) {
 | |
|                     const useSanitized = await window.ModalManager.confirm(
 | |
|                         `${filename} → ${validation.sanitized}`,
 | |
|                         'Use sanitized name?',
 | |
|                         false
 | |
|                     );
 | |
|                     if (useSanitized) {
 | |
|                         finalFilename = validation.sanitized;
 | |
|                     } else {
 | |
|                         return;
 | |
|                     }
 | |
|                 } else {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             const fullPath = `${path}/${finalFilename}`.replace(/\/+/g, '/');
 | |
|             await this.webdavClient.put(fullPath, '# New File\n\n');
 | |
| 
 | |
|             // Clear undo history since new file was created
 | |
|             if (this.fileTree.lastMoveOperation) {
 | |
|                 this.fileTree.lastMoveOperation = null;
 | |
|             }
 | |
| 
 | |
|             await this.fileTree.load();
 | |
|             showNotification(`Created ${finalFilename}`, 'success');
 | |
|             await this.editor.loadFile(fullPath);
 | |
|         },
 | |
| 
 | |
|         'new-folder': async function (path, isDir) {
 | |
|             if (!isDir) return;
 | |
| 
 | |
|             const foldername = await window.ModalManager.prompt(
 | |
|                 'Enter folder name (lowercase, underscore only):',
 | |
|                 'new_folder',
 | |
|                 'New Folder'
 | |
|             );
 | |
| 
 | |
|             if (!foldername) return;
 | |
| 
 | |
|             let finalFoldername = foldername;
 | |
|             const validation = this.validateFileName(foldername, true);
 | |
| 
 | |
|             if (!validation.valid) {
 | |
|                 showNotification(validation.message, 'warning');
 | |
| 
 | |
|                 if (validation.sanitized) {
 | |
|                     const useSanitized = await window.ModalManager.confirm(
 | |
|                         `${foldername} → ${validation.sanitized}`,
 | |
|                         'Use sanitized name?',
 | |
|                         false
 | |
|                     );
 | |
|                     if (useSanitized) {
 | |
|                         finalFoldername = validation.sanitized;
 | |
|                     } else {
 | |
|                         return;
 | |
|                     }
 | |
|                 } else {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             const fullPath = `${path}/${finalFoldername}`.replace(/\/+/g, '/');
 | |
|             await this.webdavClient.mkcol(fullPath);
 | |
| 
 | |
|             // Clear undo history since new folder was created
 | |
|             if (this.fileTree.lastMoveOperation) {
 | |
|                 this.fileTree.lastMoveOperation = null;
 | |
|             }
 | |
| 
 | |
|             await this.fileTree.load();
 | |
|             showNotification(`Created folder ${finalFoldername}`, 'success');
 | |
|         },
 | |
| 
 | |
|         rename: async function (path, isDir) {
 | |
|             const oldName = path.split('/').pop();
 | |
|             const newName = await window.ModalManager.prompt(
 | |
|                 'Rename to:',
 | |
|                 oldName,
 | |
|                 'Rename'
 | |
|             );
 | |
| 
 | |
|             if (newName && newName !== oldName) {
 | |
|                 const parentPath = path.substring(0, path.lastIndexOf('/'));
 | |
|                 const newPath = parentPath ? `${parentPath}/${newName}` : newName;
 | |
|                 await this.webdavClient.move(path, newPath);
 | |
| 
 | |
|                 // Clear undo history since manual rename occurred
 | |
|                 if (this.fileTree.lastMoveOperation) {
 | |
|                     this.fileTree.lastMoveOperation = null;
 | |
|                 }
 | |
| 
 | |
|                 await this.fileTree.load();
 | |
|                 showNotification('Renamed', 'success');
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         copy: async function (path, isDir) {
 | |
|             this.clipboard = { path, operation: 'copy', isDirectory: isDir };
 | |
|             // No notification for copy - it's a quick operation
 | |
|             this.updatePasteMenuItem();
 | |
|         },
 | |
| 
 | |
|         cut: async function (path, isDir) {
 | |
|             this.clipboard = { path, operation: 'cut', isDirectory: isDir };
 | |
|             // No notification for cut - it's a quick operation
 | |
|             this.updatePasteMenuItem();
 | |
|         },
 | |
| 
 | |
|         paste: async function (targetPath, isDir) {
 | |
|             if (!this.clipboard || !isDir) return;
 | |
| 
 | |
|             const itemName = this.clipboard.path.split('/').pop();
 | |
|             const destPath = `${targetPath}/${itemName}`.replace(/\/+/g, '/');
 | |
| 
 | |
|             if (this.clipboard.operation === 'copy') {
 | |
|                 await this.webdavClient.copy(this.clipboard.path, destPath);
 | |
|                 // No notification for paste - file tree updates show the result
 | |
|             } else {
 | |
|                 await this.webdavClient.move(this.clipboard.path, destPath);
 | |
|                 this.clipboard = null;
 | |
|                 this.updatePasteMenuItem();
 | |
|                 // No notification for move - file tree updates show the result
 | |
|             }
 | |
| 
 | |
|             await this.fileTree.load();
 | |
|         },
 | |
| 
 | |
|         delete: async function (path, isDir) {
 | |
|             const name = path.split('/').pop() || this.webdavClient.currentCollection;
 | |
|             const type = isDir ? 'folder' : 'file';
 | |
| 
 | |
|             // Check if this is a root-level collection (empty path or single-level path)
 | |
|             const pathParts = path.split('/').filter(p => p.length > 0);
 | |
|             const isCollection = pathParts.length === 0;
 | |
| 
 | |
|             if (isCollection) {
 | |
|                 // Deleting a collection - use backend API
 | |
|                 const confirmed = await window.ModalManager.confirm(
 | |
|                     `Are you sure you want to delete the collection "${name}"? This will delete all files and folders in it.`,
 | |
|                     'Delete Collection?',
 | |
|                     true
 | |
|                 );
 | |
| 
 | |
|                 if (!confirmed) return;
 | |
| 
 | |
|                 try {
 | |
|                     // Call backend API to delete collection
 | |
|                     const response = await fetch(`/api/collections/${name}`, {
 | |
|                         method: 'DELETE'
 | |
|                     });
 | |
| 
 | |
|                     if (!response.ok) {
 | |
|                         const error = await response.text();
 | |
|                         throw new Error(error || 'Failed to delete collection');
 | |
|                     }
 | |
| 
 | |
|                     showNotification(`Collection "${name}" deleted successfully`, 'success');
 | |
| 
 | |
|                     // Reload the page to refresh collections list
 | |
|                     window.location.href = '/';
 | |
|                 } catch (error) {
 | |
|                     Logger.error('Failed to delete collection:', error);
 | |
|                     showNotification(`Failed to delete collection: ${error.message}`, 'error');
 | |
|                 }
 | |
|             } else {
 | |
|                 // Deleting a regular file/folder - use WebDAV
 | |
|                 const confirmed = await window.ModalManager.confirm(
 | |
|                     `Are you sure you want to delete ${name}?`,
 | |
|                     `Delete this ${type}?`,
 | |
|                     true
 | |
|                 );
 | |
| 
 | |
|                 if (!confirmed) return;
 | |
| 
 | |
|                 await this.webdavClient.delete(path);
 | |
| 
 | |
|                 // Clear undo history since manual delete occurred
 | |
|                 if (this.fileTree.lastMoveOperation) {
 | |
|                     this.fileTree.lastMoveOperation = null;
 | |
|                 }
 | |
| 
 | |
|                 await this.fileTree.load();
 | |
|                 showNotification(`Deleted ${name}`, 'success');
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         download: async function (path, isDir) {
 | |
|             Logger.info(`Downloading ${isDir ? 'folder' : 'file'}: ${path}`);
 | |
| 
 | |
|             if (isDir) {
 | |
|                 await this.fileTree.downloadFolder(path);
 | |
|             } else {
 | |
|                 await this.fileTree.downloadFile(path);
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         upload: async function (path, isDir) {
 | |
|             if (!isDir) return;
 | |
| 
 | |
|             const input = document.createElement('input');
 | |
|             input.type = 'file';
 | |
|             input.multiple = true;
 | |
| 
 | |
|             input.onchange = async (e) => {
 | |
|                 const files = Array.from(e.target.files);
 | |
|                 for (const file of files) {
 | |
|                     const fullPath = `${path}/${file.name}`.replace(/\/+/g, '/');
 | |
|                     const content = await file.arrayBuffer();
 | |
|                     await this.webdavClient.putBinary(fullPath, content);
 | |
|                     showNotification(`Uploaded ${file.name}`, 'success');
 | |
|                 }
 | |
|                 await this.fileTree.load();
 | |
|             };
 | |
| 
 | |
|             input.click();
 | |
|         },
 | |
| 
 | |
|         'copy-to-collection': async function (path, isDir) {
 | |
|             // Get list of available collections
 | |
|             const collections = await this.webdavClient.getCollections();
 | |
|             const currentCollection = this.webdavClient.currentCollection;
 | |
| 
 | |
|             // Filter out current collection
 | |
|             const otherCollections = collections.filter(c => c !== currentCollection);
 | |
| 
 | |
|             if (otherCollections.length === 0) {
 | |
|                 showNotification('No other collections available', 'warning');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Show collection selection dialog
 | |
|             const targetCollection = await this.showCollectionSelectionDialog(
 | |
|                 otherCollections,
 | |
|                 `Copy ${PathUtils.getFileName(path)} to collection:`
 | |
|             );
 | |
| 
 | |
|             if (!targetCollection) return;
 | |
| 
 | |
|             // Copy the file/folder
 | |
|             await this.copyToCollection(path, isDir, currentCollection, targetCollection);
 | |
|         },
 | |
| 
 | |
|         'move-to-collection': async function (path, isDir) {
 | |
|             // Get list of available collections
 | |
|             const collections = await this.webdavClient.getCollections();
 | |
|             const currentCollection = this.webdavClient.currentCollection;
 | |
| 
 | |
|             // Filter out current collection
 | |
|             const otherCollections = collections.filter(c => c !== currentCollection);
 | |
| 
 | |
|             if (otherCollections.length === 0) {
 | |
|                 showNotification('No other collections available', 'warning');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // Show collection selection dialog
 | |
|             const targetCollection = await this.showCollectionSelectionDialog(
 | |
|                 otherCollections,
 | |
|                 `Move ${PathUtils.getFileName(path)} to collection:`
 | |
|             );
 | |
| 
 | |
|             if (!targetCollection) return;
 | |
| 
 | |
|             // Move the file/folder
 | |
|             await this.moveToCollection(path, isDir, currentCollection, targetCollection);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     // Old deprecated modal methods removed - all modals now use window.ModalManager
 | |
| 
 | |
|     updatePasteMenuItem() {
 | |
|         const pasteItem = document.getElementById('pasteMenuItem');
 | |
|         if (pasteItem) {
 | |
|             pasteItem.style.display = this.clipboard ? 'flex' : 'none';
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Show a dialog to select a collection
 | |
|      * @param {Array<string>} collections - List of collection names
 | |
|      * @param {string} message - Dialog message
 | |
|      * @returns {Promise<string|null>} Selected collection or null if cancelled
 | |
|      */
 | |
|     async showCollectionSelectionDialog(collections, message) {
 | |
|         // Prevent duplicate modals
 | |
|         if (this._collectionModalShowing) {
 | |
|             Logger.warn('Collection selection modal is already showing');
 | |
|             return null;
 | |
|         }
 | |
|         this._collectionModalShowing = true;
 | |
| 
 | |
|         // Create a custom modal with radio buttons for collection selection
 | |
|         const modal = document.createElement('div');
 | |
|         modal.className = 'modal fade';
 | |
|         modal.innerHTML = `
 | |
|             <div class="modal-dialog modal-dialog-centered">
 | |
|                 <div class="modal-content">
 | |
|                     <div class="modal-header">
 | |
|                         <h5 class="modal-title"><i class="bi bi-folder-symlink"></i> Select Collection</h5>
 | |
|                         <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
 | |
|                     </div>
 | |
|                     <div class="modal-body">
 | |
|                         <p class="mb-3">${message}</p>
 | |
|                         <div class="collection-list" style="max-height: 300px; overflow-y: auto;">
 | |
|                             ${collections.map((c, i) => `
 | |
|                                 <div class="form-check p-2 mb-2 rounded border collection-option" style="cursor: pointer; transition: all 0.2s;">
 | |
|                                     <input class="form-check-input" type="radio" name="collection" id="collection-${i}" value="${c}" ${i === 0 ? 'checked' : ''}>
 | |
|                                     <label class="form-check-label w-100" for="collection-${i}" style="cursor: pointer;">
 | |
|                                         <i class="bi bi-folder"></i> <strong>${c}</strong>
 | |
|                                     </label>
 | |
|                                 </div>
 | |
|                             `).join('')}
 | |
|                         </div>
 | |
|                         <div id="confirmationPreview" class="alert alert-info mt-3" style="display: none;">
 | |
|                             <i class="bi bi-info-circle"></i> <span id="confirmationText"></span>
 | |
|                         </div>
 | |
|                     </div>
 | |
|                     <div class="modal-footer">
 | |
|                         <button type="button" class="btn-flat btn-flat-secondary" data-bs-dismiss="modal">
 | |
|                             <i class="bi bi-x-circle"></i> Cancel
 | |
|                         </button>
 | |
|                         <button type="button" class="btn-flat btn-flat-primary" id="confirmCollectionBtn">
 | |
|                             <i class="bi bi-check-circle"></i> OK
 | |
|                         </button>
 | |
|                     </div>
 | |
|                 </div>
 | |
|             </div>
 | |
|         `;
 | |
| 
 | |
|         document.body.appendChild(modal);
 | |
|         const bsModal = new bootstrap.Modal(modal);
 | |
| 
 | |
|         // Extract file name and action from message
 | |
|         // Message format: "Copy filename to collection:" or "Move filename to collection:"
 | |
|         const messageMatch = message.match(/(Copy|Move)\s+(.+?)\s+to collection:/);
 | |
|         const action = messageMatch ? messageMatch[1].toLowerCase() : 'copy';
 | |
|         const fileName = messageMatch ? messageMatch[2] : 'item';
 | |
| 
 | |
|         // Get confirmation preview elements
 | |
|         const confirmationPreview = modal.querySelector('#confirmationPreview');
 | |
|         const confirmationText = modal.querySelector('#confirmationText');
 | |
| 
 | |
|         // Function to update confirmation message
 | |
|         const updateConfirmation = (collectionName) => {
 | |
|             confirmationText.textContent = `"${fileName}" will be ${action}d to "${collectionName}"`;
 | |
|             confirmationPreview.style.display = 'block';
 | |
|         };
 | |
| 
 | |
|         // Add hover effects and click handlers for collection options
 | |
|         const collectionOptions = modal.querySelectorAll('.collection-option');
 | |
|         collectionOptions.forEach(option => {
 | |
|             // Hover effect
 | |
|             option.addEventListener('mouseenter', () => {
 | |
|                 option.style.backgroundColor = 'var(--bs-light)';
 | |
|                 option.style.borderColor = 'var(--bs-primary)';
 | |
|             });
 | |
|             option.addEventListener('mouseleave', () => {
 | |
|                 const radio = option.querySelector('input[type="radio"]');
 | |
|                 if (!radio.checked) {
 | |
|                     option.style.backgroundColor = '';
 | |
|                     option.style.borderColor = '';
 | |
|                 }
 | |
|             });
 | |
| 
 | |
|             // Click on the whole div to select
 | |
|             option.addEventListener('click', () => {
 | |
|                 const radio = option.querySelector('input[type="radio"]');
 | |
|                 radio.checked = true;
 | |
| 
 | |
|                 // Update confirmation message
 | |
|                 updateConfirmation(radio.value);
 | |
| 
 | |
|                 // Update all options styling
 | |
|                 collectionOptions.forEach(opt => {
 | |
|                     const r = opt.querySelector('input[type="radio"]');
 | |
|                     if (r.checked) {
 | |
|                         opt.style.backgroundColor = 'var(--bs-primary-bg-subtle)';
 | |
|                         opt.style.borderColor = 'var(--bs-primary)';
 | |
|                     } else {
 | |
|                         opt.style.backgroundColor = '';
 | |
|                         opt.style.borderColor = '';
 | |
|                     }
 | |
|                 });
 | |
|             });
 | |
| 
 | |
|             // Set initial styling for checked option
 | |
|             const radio = option.querySelector('input[type="radio"]');
 | |
|             if (radio.checked) {
 | |
|                 option.style.backgroundColor = 'var(--bs-primary-bg-subtle)';
 | |
|                 option.style.borderColor = 'var(--bs-primary)';
 | |
|                 // Show initial confirmation
 | |
|                 updateConfirmation(radio.value);
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         return new Promise((resolve) => {
 | |
|             const confirmBtn = modal.querySelector('#confirmCollectionBtn');
 | |
| 
 | |
|             confirmBtn.addEventListener('click', () => {
 | |
|                 const selected = modal.querySelector('input[name="collection"]:checked');
 | |
|                 this._collectionModalShowing = false;
 | |
|                 bsModal.hide();
 | |
|                 resolve(selected ? selected.value : null);
 | |
|             });
 | |
| 
 | |
|             modal.addEventListener('hidden.bs.modal', () => {
 | |
|                 modal.remove();
 | |
|                 this._collectionModalShowing = false;
 | |
|                 resolve(null);
 | |
|             });
 | |
| 
 | |
|             bsModal.show();
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Copy a file or folder to another collection
 | |
|      */
 | |
|     async copyToCollection(path, isDir, sourceCollection, targetCollection) {
 | |
|         try {
 | |
|             Logger.info(`Copying ${path} from ${sourceCollection} to ${targetCollection}`);
 | |
| 
 | |
|             if (isDir) {
 | |
|                 // Copy folder recursively
 | |
|                 await this.copyFolderToCollection(path, sourceCollection, targetCollection);
 | |
|             } else {
 | |
|                 // Copy single file
 | |
|                 await this.copyFileToCollection(path, sourceCollection, targetCollection);
 | |
|             }
 | |
| 
 | |
|             showNotification(`Copied to ${targetCollection}`, 'success');
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to copy to collection:', error);
 | |
|             showNotification('Failed to copy to collection', 'error');
 | |
|             throw error;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Move a file or folder to another collection
 | |
|      */
 | |
|     async moveToCollection(path, isDir, sourceCollection, targetCollection) {
 | |
|         try {
 | |
|             Logger.info(`Moving ${path} from ${sourceCollection} to ${targetCollection}`);
 | |
| 
 | |
|             // First copy
 | |
|             await this.copyToCollection(path, isDir, sourceCollection, targetCollection);
 | |
| 
 | |
|             // Then delete from source
 | |
|             await this.webdavClient.delete(path);
 | |
|             await this.fileTree.load();
 | |
| 
 | |
|             showNotification(`Moved to ${targetCollection}`, 'success');
 | |
|         } catch (error) {
 | |
|             Logger.error('Failed to move to collection:', error);
 | |
|             showNotification('Failed to move to collection', 'error');
 | |
|             throw error;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Copy a single file to another collection
 | |
|      */
 | |
|     async copyFileToCollection(path, sourceCollection, targetCollection) {
 | |
|         // Read file from source collection
 | |
|         const content = await this.webdavClient.get(path);
 | |
| 
 | |
|         // Write to target collection
 | |
|         const originalCollection = this.webdavClient.currentCollection;
 | |
|         this.webdavClient.setCollection(targetCollection);
 | |
| 
 | |
|         // Ensure parent directories exist in target collection
 | |
|         await this.webdavClient.ensureParentDirectories(path);
 | |
| 
 | |
|         await this.webdavClient.put(path, content);
 | |
|         this.webdavClient.setCollection(originalCollection);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Copy a folder recursively to another collection
 | |
|      * @param {string} folderPath - Path of the folder to copy
 | |
|      * @param {string} sourceCollection - Source collection name
 | |
|      * @param {string} targetCollection - Target collection name
 | |
|      * @param {Set} visitedPaths - Set of already visited paths to prevent infinite loops
 | |
|      */
 | |
|     async copyFolderToCollection(folderPath, sourceCollection, targetCollection, visitedPaths = new Set()) {
 | |
|         // Prevent infinite loops by tracking visited paths
 | |
|         if (visitedPaths.has(folderPath)) {
 | |
|             Logger.warn(`Skipping already visited path: ${folderPath}`);
 | |
|             return;
 | |
|         }
 | |
|         visitedPaths.add(folderPath);
 | |
| 
 | |
|         Logger.info(`Copying folder: ${folderPath} from ${sourceCollection} to ${targetCollection}`);
 | |
| 
 | |
|         // Set to source collection to list items
 | |
|         const originalCollection = this.webdavClient.currentCollection;
 | |
|         this.webdavClient.setCollection(sourceCollection);
 | |
| 
 | |
|         // Get only direct children (not recursive to avoid infinite loop)
 | |
|         const items = await this.webdavClient.list(folderPath, false);
 | |
|         Logger.debug(`Found ${items.length} items in ${folderPath}:`, items.map(i => i.path));
 | |
| 
 | |
|         // Create the folder in target collection
 | |
|         this.webdavClient.setCollection(targetCollection);
 | |
| 
 | |
|         try {
 | |
|             // Ensure parent directories exist first
 | |
|             await this.webdavClient.ensureParentDirectories(folderPath + '/dummy.txt');
 | |
|             // Then create the folder itself
 | |
|             await this.webdavClient.createFolder(folderPath);
 | |
|             Logger.debug(`Created folder: ${folderPath}`);
 | |
|         } catch (error) {
 | |
|             // Folder might already exist (405 Method Not Allowed), ignore error
 | |
|             if (error.message && error.message.includes('405')) {
 | |
|                 Logger.debug(`Folder ${folderPath} already exists (405)`);
 | |
|             } else {
 | |
|                 Logger.debug('Folder might already exist:', error);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Copy all items
 | |
|         for (const item of items) {
 | |
|             if (item.isDirectory) {
 | |
|                 // Recursively copy subdirectory
 | |
|                 await this.copyFolderToCollection(item.path, sourceCollection, targetCollection, visitedPaths);
 | |
|             } else {
 | |
|                 // Copy file
 | |
|                 this.webdavClient.setCollection(sourceCollection);
 | |
|                 const content = await this.webdavClient.get(item.path);
 | |
|                 this.webdavClient.setCollection(targetCollection);
 | |
|                 // Ensure parent directories exist before copying file
 | |
|                 await this.webdavClient.ensureParentDirectories(item.path);
 | |
|                 await this.webdavClient.put(item.path, content);
 | |
|                 Logger.debug(`Copied file: ${item.path}`);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         this.webdavClient.setCollection(originalCollection);
 | |
|     }
 | |
| } |