/** * Directory Scanner Module * Scans directories and collects files */ (function() { 'use strict'; // Store ZIP data for later access const zipCache = new Map(); // path -> { zip: JSZip, fileHandle: FileSystemFileHandle } /** * Scan directory and build folder tree with files */ async function scanDirectory(dirHandle, preserveState = false) { // Save current state if preserving let savedExpanded = new Set(); let savedSelected = new Set(); if (preserveState) { savedExpanded = getExpandedPaths(window.app.folderTree); savedSelected = new Set(window.app.selectedFolders); } // Clear ZIP cache zipCache.clear(); // Map to store files by folder handle (or ZIP path for virtual folders) const foldersMap = new Map(); // Recursively scan await scanFolder(dirHandle, foldersMap, dirHandle.name); // Build tree structure window.app.folderTree = window.app.modules.tree.buildTree(dirHandle, foldersMap); // Set in store window.app.modules.store.setFolderTree(window.app.folderTree); if (preserveState) { // Restore expanded state restoreExpandedPaths(window.app.folderTree, savedExpanded); // Restore selection window.app.selectedFolders = savedSelected; // Render without changing selection window.app.modules.tree.render(); window.app.modules.store.setSelectedFolders(savedSelected); } else { // Render tree window.app.modules.tree.render(); // Auto-expand and select all folders window.app.modules.tree.expandAll(); window.app.modules.tree.selectAll(); } } /** * Get all expanded folder paths from tree */ function getExpandedPaths(folders, paths = new Set()) { for (const folder of folders) { if (folder.expanded) { paths.add(folder.path); } if (folder.children) { getExpandedPaths(folder.children, paths); } } return paths; } /** * Restore expanded state to tree */ function restoreExpandedPaths(folders, expandedPaths) { for (const folder of folders) { folder.expanded = expandedPaths.has(folder.path); if (folder.children) { restoreExpandedPaths(folder.children, expandedPaths); } } } /** * Recursively scan a folder */ async function scanFolder(dirHandle, foldersMap, currentPath) { const items = []; try { for await (const entry of dirHandle.values()) { if (entry.kind === 'file') { // Create file object const file = await createFileObject(entry, dirHandle); if (file) { items.push(file); // Check if it's a ZIP file - scan its contents if (file.extension === 'zip' && typeof JSZip !== 'undefined') { await scanZipFile(file, foldersMap, currentPath, items); } } } else if (entry.kind === 'directory') { // Add directory reference items.push({ handle: entry, isDirectory: true }); // Recursively scan subdirectory const childPath = currentPath + '/' + entry.name; await scanFolder(entry, foldersMap, childPath); } } } catch (err) { console.error('Error scanning folder:', dirHandle.name, err); } // Store files for this folder foldersMap.set(dirHandle, items); } /** * Scan a ZIP file and add its contents as virtual folders */ async function scanZipFile(zipFileObj, foldersMap, parentPath, parentItems) { try { const fileObj = await zipFileObj.handle.getFile(); const arrayBuffer = await fileObj.arrayBuffer(); const zip = await JSZip.loadAsync(arrayBuffer); const zipPath = parentPath + '/' + zddc.joinExtension(zipFileObj.originalFilename, zipFileObj.extension); // Cache the ZIP for later extraction zipCache.set(zipPath, { zip: zip, fileHandle: zipFileObj.handle, folderHandle: zipFileObj.folderHandle }); // Mark the file as a ZIP container zipFileObj.isZipContainer = true; zipFileObj.zipPath = zipPath; // Build virtual folder structure from ZIP contents const virtualFolders = new Map(); // path -> { files: [], subdirs: Set } virtualFolders.set(zipPath, { files: [], subdirs: new Set() }); zip.forEach((relativePath, zipEntry) => { if (zipEntry.dir) { // It's a directory const dirPath = zipPath + '/' + relativePath.replace(/\/$/, ''); if (!virtualFolders.has(dirPath)) { virtualFolders.set(dirPath, { files: [], subdirs: new Set() }); } // Add to parent's subdirs const parentDir = dirPath.substring(0, dirPath.lastIndexOf('/')); if (virtualFolders.has(parentDir)) { virtualFolders.get(parentDir).subdirs.add(dirPath); } } else { // It's a file const fileName = relativePath.split('/').pop(); const fileDir = relativePath.includes('/') ? zipPath + '/' + relativePath.substring(0, relativePath.lastIndexOf('/')) : zipPath; // Ensure parent directories exist ensureVirtualPath(virtualFolders, zipPath, fileDir); // Create virtual file object const split = zddc.splitExtension(fileName); const virtualFile = { originalFilename: split.name, extension: split.extension, size: zipEntry._data ? zipEntry._data.uncompressedSize : 0, lastModified: zipEntry.date ? zipEntry.date.getTime() : Date.now(), // Virtual file markers isVirtual: true, zipPath: zipPath, zipEntryPath: relativePath, // Editable fields trackingNumber: '', revision: '', status: '', title: '', // State isDirty: false, error: false, errorMessage: '', validation: null, sha256: null }; virtualFolders.get(fileDir).files.push(virtualFile); } }); // Convert virtual folders to format compatible with tree builder // Create a virtual handle for the ZIP root const zipVirtualHandle = { name: zddc.joinExtension(zipFileObj.originalFilename, zipFileObj.extension), kind: 'directory', isZipRoot: true, zipPath: zipPath }; // Store virtual folder data buildVirtualFolderMap(virtualFolders, zipPath, foldersMap, zipVirtualHandle); // Add ZIP as a virtual directory in parent parentItems.push({ handle: zipVirtualHandle, isDirectory: true, isZipRoot: true }); } catch (err) { console.error('Error scanning ZIP file:', zipFileObj.originalFilename, err); } } /** * Ensure all parent directories exist in virtual folder map */ function ensureVirtualPath(virtualFolders, zipPath, targetPath) { if (virtualFolders.has(targetPath)) return; const parts = targetPath.substring(zipPath.length + 1).split('/').filter(p => p); let currentPath = zipPath; for (const part of parts) { const parentPath = currentPath; currentPath = currentPath + '/' + part; if (!virtualFolders.has(currentPath)) { virtualFolders.set(currentPath, { files: [], subdirs: new Set() }); } if (virtualFolders.has(parentPath)) { virtualFolders.get(parentPath).subdirs.add(currentPath); } } } /** * Build virtual folder entries for the foldersMap * Uses path strings as keys for virtual folders to avoid object reference issues */ function buildVirtualFolderMap(virtualFolders, zipPath, foldersMap, zipVirtualHandle) { const rootData = virtualFolders.get(zipPath); if (!rootData) return; // Create items array for ZIP root const rootItems = [...rootData.files]; // Add subdirectories for (const subdirPath of rootData.subdirs) { const subdirName = subdirPath.split('/').pop(); const subdirHandle = { name: subdirName, kind: 'directory', isVirtualDir: true, virtualPath: subdirPath, zipPath: zipPath }; rootItems.push({ handle: subdirHandle, isDirectory: true, isVirtualDir: true }); // Recursively add subdir contents buildVirtualSubfolder(virtualFolders, subdirPath, foldersMap, zipPath); } // Store with both the handle object AND the path string as keys // This ensures lookup works regardless of which reference is used foldersMap.set(zipVirtualHandle, rootItems); foldersMap.set(zipPath, rootItems); // Path-based key for tree building } /** * Recursively build virtual subfolder entries */ function buildVirtualSubfolder(virtualFolders, folderPath, foldersMap, zipPath) { const folderData = virtualFolders.get(folderPath); if (!folderData) return; const folderName = folderPath.split('/').pop(); const folderHandle = { name: folderName, kind: 'directory', isVirtualDir: true, virtualPath: folderPath, zipPath: zipPath }; const items = [...folderData.files]; // Store with path string key for tree building lookup foldersMap.set(folderPath, items); // Add subdirectories for (const subdirPath of folderData.subdirs) { const subdirName = subdirPath.split('/').pop(); const subdirHandle = { name: subdirName, kind: 'directory', isVirtualDir: true, virtualPath: subdirPath, zipPath: zipPath }; items.push({ handle: subdirHandle, isDirectory: true, isVirtualDir: true }); // Recursively add subdir contents buildVirtualSubfolder(virtualFolders, subdirPath, foldersMap, zipPath); } foldersMap.set(folderHandle, items); } /** * Get cached ZIP data */ function getZipCache(zipPath) { return zipCache.get(zipPath); } /** * Extract a ZIP file to its parent directory */ async function extractZip(zipPath) { const cached = zipCache.get(zipPath); if (!cached) { throw new Error('ZIP not found in cache'); } const { zip, folderHandle } = cached; // Get the ZIP filename without extension for the extract folder name const zipName = zipPath.split('/').pop(); const extractFolderName = zipName.replace(/\.zip$/i, ''); // Create extraction folder const extractFolder = await folderHandle.getDirectoryHandle(extractFolderName, { create: true }); // Extract all files const entries = []; zip.forEach((relativePath, zipEntry) => { if (!zipEntry.dir) { entries.push({ path: relativePath, entry: zipEntry }); } }); for (const { path, entry } of entries) { try { // Create subdirectories if needed const parts = path.split('/'); const fileName = parts.pop(); let currentDir = extractFolder; for (const part of parts) { if (part) { currentDir = await currentDir.getDirectoryHandle(part, { create: true }); } } // Write file const content = await entry.async('arraybuffer'); const fileHandle = await currentDir.getFileHandle(fileName, { create: true }); const writable = await fileHandle.createWritable(); await writable.write(content); await writable.close(); } catch (err) { console.error('Error extracting file:', path, err); } } return extractFolderName; } /** * Create file object with metadata */ async function createFileObject(fileHandle, folderHandle) { try { const file = await fileHandle.getFile(); const split = zddc.splitExtension(file.name); return { handle: fileHandle, folderHandle: folderHandle, originalFilename: split.name, extension: split.extension, size: file.size, lastModified: file.lastModified, // Editable fields trackingNumber: '', revision: '', status: '', title: '', // State isDirty: false, error: false, errorMessage: '', validation: null, sha256: null // folderPath will be added later in buildTree }; } catch (err) { console.error('Error reading file:', fileHandle.name, err); return null; } } // Export module window.app.modules.scanner = { scanDirectory, getZipCache, extractZip }; })();