Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
281 lines
9.3 KiB
TypeScript
281 lines
9.3 KiB
TypeScript
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<string, MediaAsset['type']> = {
|
|
'.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<string, string> = {
|
|
'.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();
|
|
});
|
|
}
|
|
};
|
|
}
|