import type { Plugin } from 'vite'; import * as fs from 'fs'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; import formidable from 'formidable'; interface MediaAsset { id: string; name: string; path: string; type: 'image' | 'audio' | 'animation' | 'folder'; size?: number; mimeType?: string; createdAt: string; parentPath?: string; } /** * Media Management API Plugin * Provides API endpoints for managing media assets (images, audio, animations, folders) */ export function mediaManagementApiPlugin(): Plugin { let mediaDir: string; return { name: 'media-management-api', configureServer(server) { // Set media directory path mediaDir = path.join(process.cwd(), 'assets', 'media'); // Ensure media directory exists if (!fs.existsSync(mediaDir)) { fs.mkdirSync(mediaDir, { recursive: true }); } // Helper functions const sendJSON = (res: any, statusCode: number, data: any) => { res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(data)); }; const sendError = (res: any, statusCode: number, message: string) => { sendJSON(res, statusCode, { error: message, timestamp: new Date().toISOString() }); }; const getAssetType = (filePath: string, isDirectory: boolean): MediaAsset['type'] => { if (isDirectory) return 'folder'; const ext = path.extname(filePath).toLowerCase(); const mimeTypes: Record = { '.jpg': 'image', '.jpeg': 'image', '.png': 'image', '.gif': 'image', '.webp': 'image', '.svg': 'image', '.mp3': 'audio', '.wav': 'audio', '.ogg': 'audio', '.m4a': 'audio', '.json': 'animation', }; return mimeTypes[ext] || 'image'; }; const scanDirectory = (dirPath: string, relativePath: string = ''): MediaAsset[] => { const assets: MediaAsset[] = []; try { const items = fs.readdirSync(dirPath); for (const item of items) { // Skip hidden files if (item.startsWith('.')) continue; const fullPath = path.join(dirPath, item); const itemRelativePath = relativePath ? `${relativePath}/${item}` : item; const stats = fs.statSync(fullPath); const asset: MediaAsset = { id: uuidv4(), name: item, path: itemRelativePath, type: getAssetType(fullPath, stats.isDirectory()), createdAt: stats.birthtime.toISOString(), parentPath: relativePath || undefined, }; if (!stats.isDirectory()) { asset.size = stats.size; asset.mimeType = getMimeType(fullPath); } assets.push(asset); } } catch (error: any) { console.error('Error scanning directory:', error); } return assets; }; const getMimeType = (filePath: string): string => { const ext = path.extname(filePath).toLowerCase(); const mimeTypes: Record = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', '.json': 'application/json', }; return mimeTypes[ext] || 'application/octet-stream'; }; // Middleware to handle API routes server.middlewares.use(async (req, res, next) => { const url = req.url || ''; // GET /api/media - List media assets if (url.startsWith('/api/media') && req.method === 'GET') { try { const urlObj = new URL(url, `http://${req.headers.host}`); const requestedPath = urlObj.searchParams.get('path') || ''; const targetDir = requestedPath ? path.join(mediaDir, requestedPath) : mediaDir; if (!fs.existsSync(targetDir)) { return sendError(res, 404, 'Directory not found'); } const assets = scanDirectory(targetDir, requestedPath); sendJSON(res, 200, assets); } catch (error: any) { console.error('Error listing media:', error); sendError(res, 500, error.message || 'Failed to list media assets'); } return; } // POST /api/media/upload - Upload file if (url === '/api/media/upload' && req.method === 'POST') { try { const form = formidable({ uploadDir: mediaDir, keepExtensions: true, maxFileSize: 50 * 1024 * 1024, // 50MB }); form.parse(req, (err, fields, files) => { if (err) { return sendError(res, 400, 'Upload failed: ' + err.message); } try { const file = Array.isArray(files.file) ? files.file[0] : files.file; if (!file) { return sendError(res, 400, 'No file uploaded'); } const targetPath = Array.isArray(fields.path) ? fields.path[0] : fields.path; const targetDir = targetPath ? path.join(mediaDir, targetPath) : mediaDir; if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } const finalPath = path.join(targetDir, file.originalFilename || file.newFilename); fs.renameSync(file.filepath, finalPath); sendJSON(res, 200, { message: 'File uploaded successfully', path: path.relative(mediaDir, finalPath) }); } catch (error: any) { sendError(res, 500, 'Failed to save file: ' + error.message); } }); } catch (error: any) { sendError(res, 500, error.message || 'Upload failed'); } return; } // POST /api/media/folder - Create folder if (url === '/api/media/folder' && req.method === 'POST') { try { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { const { name, parentPath } = JSON.parse(body); if (!name || !name.trim()) { return sendError(res, 400, 'Folder name is required'); } const targetDir = parentPath ? path.join(mediaDir, parentPath, name) : path.join(mediaDir, name); if (fs.existsSync(targetDir)) { return sendError(res, 400, 'Folder already exists'); } fs.mkdirSync(targetDir, { recursive: true }); sendJSON(res, 200, { message: 'Folder created successfully', path: path.relative(mediaDir, targetDir) }); } catch (error: any) { sendError(res, 400, 'Invalid request: ' + error.message); } }); } catch (error: any) { sendError(res, 500, error.message || 'Failed to create folder'); } return; } // DELETE /api/media/:path - Delete file or folder if (url.startsWith('/api/media/') && req.method === 'DELETE') { try { const assetPath = decodeURIComponent(url.replace('/api/media/', '')); const fullPath = path.join(mediaDir, assetPath); if (!fs.existsSync(fullPath)) { return sendError(res, 404, 'Asset not found'); } const stats = fs.statSync(fullPath); if (stats.isDirectory()) { fs.rmSync(fullPath, { recursive: true, force: true }); } else { fs.unlinkSync(fullPath); } sendJSON(res, 200, { message: 'Asset deleted successfully' }); } catch (error: any) { console.error('Error deleting asset:', error); sendError(res, 500, error.message || 'Failed to delete asset'); } return; } // GET /api/media/file/:path - Serve file if (url.startsWith('/api/media/file/') && req.method === 'GET') { try { const assetPath = decodeURIComponent(url.replace('/api/media/file/', '')); const fullPath = path.join(mediaDir, assetPath); if (!fs.existsSync(fullPath) || fs.statSync(fullPath).isDirectory()) { return sendError(res, 404, 'File not found'); } const mimeType = getMimeType(fullPath); res.setHeader('Content-Type', mimeType); fs.createReadStream(fullPath).pipe(res); } catch (error: any) { console.error('Error serving file:', error); sendError(res, 500, error.message || 'Failed to serve file'); } return; } next(); }); } }; }