import express from 'express'; import { existsSync } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const PORT = Number(process.env.PORT ?? 3001); const MAX_ENTRIES = Number(process.env.DRIVEBOARD_MAX_ENTRIES ?? 12000); const LOCAL_OWNER = 'Local file system'; const imageExtensions = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'avif']); const videoExtensions = new Set(['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v']); const pdfExtensions = new Set(['pdf']); const archiveExtensions = new Set(['zip', 'rar', '7z', 'tar', 'gz']); const spreadsheetExtensions = new Set(['xls', 'xlsx', 'csv', 'tsv', 'ods']); const slideExtensions = new Set(['ppt', 'pptx', 'key']); const documentExtensions = new Set(['doc', 'docx', 'txt', 'md', 'rtf', 'odt']); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); const distPath = path.join(projectRoot, 'dist'); function compareDirents(left, right) { if (left.isDirectory() !== right.isDirectory()) { return left.isDirectory() ? -1 : 1; } return left.name.localeCompare(right.name); } function createItemId(kind, pathSegments) { return `${kind}:${pathSegments.join('/')}`; } function inferFileType(fileName) { const extension = fileName.split('.').pop()?.toLowerCase() ?? ''; if (imageExtensions.has(extension)) { return 'image'; } if (videoExtensions.has(extension)) { return 'video'; } if (pdfExtensions.has(extension)) { return 'pdf'; } if (archiveExtensions.has(extension)) { return 'zip'; } if (spreadsheetExtensions.has(extension)) { return 'sheet'; } if (slideExtensions.has(extension)) { return 'slides'; } if (documentExtensions.has(extension)) { return 'doc'; } return 'file'; } function formatBytes(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } const digits = value >= 10 || unitIndex === 0 ? 0 : 1; return `${value.toFixed(digits)} ${units[unitIndex]}`; } function formatTimestamp(timestamp) { if (!timestamp) { return 'Unavailable'; } return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', }).format(new Date(timestamp)); } async function readFileNode(fullPath, pathSegments) { const stat = await fs.stat(fullPath); return { id: createItemId('file', pathSegments), name: path.basename(fullPath), kind: 'file', type: inferFileType(path.basename(fullPath)), owner: LOCAL_OWNER, modified: formatTimestamp(stat.mtimeMs), modifiedMs: stat.mtimeMs, size: formatBytes(stat.size), sizeBytes: stat.size, }; } async function readDirectoryNode(fullPath, pathSegments, state) { const directoryStat = await fs.stat(fullPath); const children = []; let folderCount = 0; let fileCount = 0; let sizeBytes = 0; let latestModified = directoryStat.mtimeMs ?? 0; const entries = await fs.readdir(fullPath, { withFileTypes: true }); entries.sort(compareDirents); for (const entry of entries) { if (state.entryCount >= state.maxEntries) { state.truncated = true; break; } if (entry.isSymbolicLink()) { continue; } const entryFullPath = path.join(fullPath, entry.name); const entryPathSegments = [...pathSegments, entry.name]; state.entryCount += 1; try { if (entry.isDirectory()) { const childFolder = await readDirectoryNode(entryFullPath, entryPathSegments, state); children.push(childFolder); folderCount += childFolder.folderCount + 1; fileCount += childFolder.fileCount; sizeBytes += childFolder.sizeBytes ?? 0; latestModified = Math.max(latestModified, childFolder.modifiedMs ?? 0); continue; } if (entry.isFile()) { const fileNode = await readFileNode(entryFullPath, entryPathSegments); children.push(fileNode); fileCount += 1; sizeBytes += fileNode.sizeBytes ?? 0; latestModified = Math.max(latestModified, fileNode.modifiedMs ?? 0); } } catch (error) { state.warnings.push(`Skipped ${entryFullPath}: ${error.message}`); } } return { id: pathSegments.length ? createItemId('folder', pathSegments) : 'root', name: pathSegments[pathSegments.length - 1] || path.basename(fullPath) || fullPath, kind: 'folder', owner: LOCAL_OWNER, modified: formatTimestamp(latestModified), modifiedMs: latestModified, size: formatBytes(sizeBytes), sizeBytes, folderCount, fileCount, children, }; } async function scanLocalDirectory(directoryPath) { const resolvedPath = path.resolve(directoryPath); const stat = await fs.stat(resolvedPath); if (!stat.isDirectory()) { const error = new Error('The provided path is not a directory.'); error.statusCode = 400; throw error; } const state = { entryCount: 0, maxEntries: MAX_ENTRIES, truncated: false, warnings: [], }; const tree = await readDirectoryNode(resolvedPath, [], state); return { tree: { ...tree, id: 'root', name: path.basename(resolvedPath) || resolvedPath, }, directoryPath: resolvedPath, truncated: state.truncated, scannedEntries: state.entryCount, warnings: state.warnings.slice(0, 5), }; } const app = express(); app.use(express.json({ limit: '1mb' })); app.get('/api/health', (_request, response) => { response.json({ ok: true }); }); app.post('/api/filesystem/scan', async (request, response) => { const directoryPath = typeof request.body?.directoryPath === 'string' ? request.body.directoryPath.trim() : ''; if (!directoryPath) { response.status(400).json({ error: 'Directory path is required.' }); return; } try { const result = await scanLocalDirectory(directoryPath); response.json(result); } catch (error) { if (error?.code === 'ENOENT') { response.status(404).json({ error: `Directory not found: ${path.resolve(directoryPath)}` }); return; } if (error?.code === 'EACCES' || error?.code === 'EPERM') { response.status(403).json({ error: `Permission denied for: ${path.resolve(directoryPath)}` }); return; } response.status(error?.statusCode ?? 500).json({ error: error?.message ?? 'Unable to scan the requested directory.' }); } }); if (existsSync(distPath)) { app.use(express.static(distPath)); app.get(/^(?!\/api).*/, (_request, response) => { response.sendFile(path.join(distPath, 'index.html')); }); } app.listen(PORT, () => { console.log(`Driveboard local filesystem API listening on http://127.0.0.1:${PORT}`); });