feat: sync full workspace including web modules, docs, and configurations to Gitea
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>
This commit is contained in:
280
axhub-make/vite-plugins/mediaManagementApiPlugin.ts
Normal file
280
axhub-make/vite-plugins/mediaManagementApiPlugin.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user