const fs = require('fs'); const path = require('path'); const { promisify } = require('util'); const copyFile = promisify(fs.copyFile); const mkdir = promisify(fs.mkdir); const readdir = promisify(fs.readdir); /** * FileDeployer - Handles copying dashboard and API files to installation directory * * In dev mode: reads from sibling directories (../../../status, ../../../dashcaddy-api) * In packaged exe: reads from extraResources (process.resourcesPath/status, .../dashcaddy-api) */ class FileDeployer { constructor() { this._resolveSourcePaths(); } _resolveSourcePaths() { let app; try { app = require('electron').app; } catch { // Not in Electron context } const isPackaged = app?.isPackaged || false; if (isPackaged) { // Packaged exe: files are in extraResources this.dashboardSource = path.join(process.resourcesPath, 'status'); this.apiSource = path.join(process.resourcesPath, 'dashcaddy-api'); } else { // Dev mode: sibling directories in the project tree this.dashboardSource = path.join(__dirname, '../../../status'); this.apiSource = path.join(__dirname, '../../../dashcaddy-api'); } } /** * Copy directory recursively, skipping node_modules, .git, and test files */ async copyDirectory(src, dest, progressCallback = null) { try { await mkdir(dest, { recursive: true }); const entries = await readdir(src, { withFileTypes: true }); let copiedCount = 0; for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); // Skip unnecessary directories and files if (['node_modules', '.git', '__tests__', 'test', '.nyc_output', 'coverage'].includes(entry.name)) { continue; } // Skip test files if (entry.name.endsWith('.test.js') || entry.name.endsWith('.spec.js')) { continue; } if (entry.isDirectory()) { const subResult = await this.copyDirectory(srcPath, destPath, progressCallback); if (subResult.success) copiedCount += subResult.filesCopied; } else { await copyFile(srcPath, destPath); copiedCount++; if (progressCallback) { progressCallback({ type: 'file', source: srcPath, destination: destPath, count: copiedCount }); } } } return { success: true, filesCopied: copiedCount }; } catch (error) { return { success: false, error: error.message }; } } /** * Deploy dashboard files to installation directory */ async deployDashboard(installPath, progressCallback = null) { try { const dashboardDest = path.join(installPath, 'sites', 'status'); if (progressCallback) { progressCallback({ type: 'start', message: 'Deploying dashboard files...', source: this.dashboardSource, destination: dashboardDest }); } // Verify source exists if (!fs.existsSync(this.dashboardSource)) { throw new Error(`Dashboard source not found at: ${this.dashboardSource}`); } const result = await this.copyDirectory( this.dashboardSource, dashboardDest, progressCallback ); if (!result.success) { throw new Error(result.error); } if (progressCallback) { progressCallback({ type: 'complete', message: `Dashboard deployed (${result.filesCopied} files)`, filesCopied: result.filesCopied }); } return { success: true, path: dashboardDest, filesCopied: result.filesCopied }; } catch (error) { return { success: false, error: error.message }; } } /** * Deploy API server files to installation directory */ async deployAPI(installPath, progressCallback = null) { try { const apiDest = path.join(installPath, 'sites', 'dashcaddy-api'); if (progressCallback) { progressCallback({ type: 'start', message: 'Deploying API server files...', source: this.apiSource, destination: apiDest }); } if (!fs.existsSync(this.apiSource)) { throw new Error(`API source not found at: ${this.apiSource}`); } const result = await this.copyDirectory( this.apiSource, apiDest, progressCallback ); if (!result.success) { throw new Error(result.error); } if (progressCallback) { progressCallback({ type: 'complete', message: `API server deployed (${result.filesCopied} files)`, filesCopied: result.filesCopied }); } return { success: true, path: apiDest, filesCopied: result.filesCopied }; } catch (error) { return { success: false, error: error.message }; } } /** * Deploy both dashboard and API files */ async deployComplete(installPath, progressCallback = null) { try { const dashboardResult = await this.deployDashboard(installPath, progressCallback); if (!dashboardResult.success) { throw new Error(`Dashboard deployment failed: ${dashboardResult.error}`); } const apiResult = await this.deployAPI(installPath, progressCallback); if (!apiResult.success) { throw new Error(`API deployment failed: ${apiResult.error}`); } return { success: true, dashboard: dashboardResult, api: apiResult }; } catch (error) { return { success: false, error: error.message }; } } /** * Check if source directories exist */ async validateSources() { const checks = { dashboard: fs.existsSync(this.dashboardSource), api: fs.existsSync(this.apiSource) }; return { valid: checks.dashboard && checks.api, checks, paths: { dashboard: this.dashboardSource, api: this.apiSource } }; } } module.exports = FileDeployer;