const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const AdmZip = require('adm-zip'); const processManager = require('./processManager'); const nginxManager = require('./nginxManager'); const config = require('../config'); // Use absolute paths for container environment const DATA_DIR = '/app/data'; const PROJECTS_FILE = path.join(DATA_DIR, 'projects.json'); const LOGS_DIR = path.join(DATA_DIR, 'logs'); const DEPLOY_DIR = '/app/projects'; if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } if (!fs.existsSync(LOGS_DIR)) { fs.mkdirSync(LOGS_DIR, { recursive: true }); } if (!fs.existsSync(DEPLOY_DIR)) { fs.mkdirSync(DEPLOY_DIR, { recursive: true }); } const getProjects = () => { // Ensure directory exists before writing if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } if (!fs.existsSync(PROJECTS_FILE)) { fs.writeFileSync(PROJECTS_FILE, JSON.stringify([])); return []; } return JSON.parse(fs.readFileSync(PROJECTS_FILE, 'utf8')); }; const saveProjects = (projects) => { fs.writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2)); }; const addLog = (projectId, message, type = 'info') => { const logFile = path.join(LOGS_DIR, `${projectId}.json`); const logs = fs.existsSync(logFile) ? JSON.parse(fs.readFileSync(logFile, 'utf8')) : []; logs.push({ timestamp: new Date().toISOString(), message, type }); fs.writeFileSync(logFile, JSON.stringify(logs, null, 2)); }; const calculateFileHash = (filePath) => { const content = fs.readFileSync(filePath); return crypto.createHash('md5').update(content).digest('hex'); }; const fixHtmlPaths = (projectDir) => { const htmlFiles = getAllFiles(projectDir).filter(f => f.endsWith('.html')); for (const htmlFile of htmlFiles) { let content = fs.readFileSync(htmlFile, 'utf8'); let modified = false; // Fix absolute paths in HTML attributes // src="/assets/..." -> src="./assets/..." // href="/assets/..." -> href="./assets/..." // Also fix other common static resource paths const patterns = [ { regex: /src="\/(?!\/|http)/g, replacement: 'src="./' }, { regex: /href="\/(?!\/|http)/g, replacement: 'href="./' }, { regex: /src='\/(?!\/|http)/g, replacement: "src='./" }, { regex: /href='\/(?!\/|http)/g, replacement: "href='./" }, ]; for (const { regex, replacement } of patterns) { if (regex.test(content)) { content = content.replace(regex, replacement); modified = true; } } if (modified) { fs.writeFileSync(htmlFile, content); console.log(`Fixed paths in ${htmlFile}`); } } }; const getAllProjects = () => { return getProjects(); }; const getProjectById = (id) => { const projects = getProjects(); return projects.find(p => p.id === id); }; const createProject = ({ name, description, files }) => { const projects = getProjects(); const id = Date.now().toString(); const projectDir = path.join(DEPLOY_DIR, id); fs.mkdirSync(projectDir, { recursive: true }); const fileInfos = []; files.forEach(file => { const destPath = path.join(projectDir, file.originalname); fs.copyFileSync(file.path, destPath); fs.unlinkSync(file.path); if (file.originalname.endsWith('.zip')) { try { const zip = new AdmZip(destPath); zip.extractAllTo(projectDir, true); fs.unlinkSync(destPath); addLog(id, `Extracted ${file.originalname}`, 'success'); } catch (e) { addLog(id, `Failed to extract ${file.originalname}: ${e.message}`, 'error'); } } else { fileInfos.push({ name: file.originalname, size: file.size, hash: calculateFileHash(destPath) }); } }); const allFiles = getAllFiles(projectDir); // Fix absolute paths in HTML files to relative paths fixHtmlPaths(projectDir); const project = { id, name, description, files: allFiles.map(f => ({ name: path.relative(projectDir, f), size: fs.statSync(f).size, hash: calculateFileHash(f) })), status: 'stopped', port: null, url: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastDeployed: null }; projects.push(project); saveProjects(projects); addLog(id, `Project "${name}" created with ${allFiles.length} file(s)`, 'success'); return project; }; const getAllFiles = (dir) => { const files = []; const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); if (fs.statSync(fullPath).isDirectory()) { files.push(...getAllFiles(fullPath)); } else { files.push(fullPath); } } return files; }; const updateProject = (id, updates) => { const projects = getProjects(); const index = projects.findIndex(p => p.id === id); if (index === -1) return null; projects[index] = { ...projects[index], ...updates, updatedAt: new Date().toISOString() }; saveProjects(projects); addLog(id, `Project updated: ${JSON.stringify(updates)}`); return projects[index]; }; const deleteProject = (id) => { const projects = getProjects(); const index = projects.findIndex(p => p.id === id); if (index === -1) return false; const project = projects[index]; if (project.status === 'running') { processManager.stopProject(project); } if (config.useNginx === true || config.useNginx === 'true') { nginxManager.removeProjectLocation(id); } const projectDir = path.join(DEPLOY_DIR, id); if (fs.existsSync(projectDir)) { fs.rmSync(projectDir, { recursive: true, force: true }); } const logFile = path.join(LOGS_DIR, `${id}.json`); if (fs.existsSync(logFile)) { fs.unlinkSync(logFile); } projects.splice(index, 1); saveProjects(projects); return true; }; const addFilesToProject = (id, files) => { const projects = getProjects(); const project = projects.find(p => p.id === id); if (!project) return null; const projectDir = path.join(DEPLOY_DIR, id); const newFileInfos = files.map(file => { const destPath = path.join(projectDir, file.originalname); fs.copyFileSync(file.path, destPath); fs.unlinkSync(file.path); return { name: file.originalname, size: file.size, hash: calculateFileHash(destPath) }; }); project.files = [...project.files, ...newFileInfos]; project.updatedAt = new Date().toISOString(); saveProjects(projects); addLog(id, `Added ${files.length} file(s) to project`); return project; }; const updateProjectStatus = (id, status, port = null, url = null) => { const projects = getProjects(); const project = projects.find(p => p.id === id); if (!project) return null; project.status = status; project.port = port; project.url = url || config.getProjectUrl(id, port); if (status === 'running') { project.lastDeployed = new Date().toISOString(); addLog(id, `Project started on port ${port}`, 'success'); } else if (status === 'stopped') { addLog(id, 'Project stopped'); } saveProjects(projects); return project; }; const getProjectLogs = (id) => { const logFile = path.join(LOGS_DIR, `${id}.json`); if (!fs.existsSync(logFile)) { return []; } return JSON.parse(fs.readFileSync(logFile, 'utf8')); }; module.exports = { getAllProjects, getProjectById, createProject, updateProject, deleteProject, addFilesToProject, updateProjectStatus, getProjectLogs };