mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-06 21:52:43 +08:00
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
import fsSync, { promises as fs } from 'node:fs';
|
|
|
|
import express from 'express';
|
|
import mime from 'mime-types';
|
|
import multer from 'multer';
|
|
|
|
import {
|
|
buildStoredImageRecords,
|
|
ensureImageAssetsDir,
|
|
isAllowedImageMimeType,
|
|
resolveImageAssetFile,
|
|
} from '@/modules/assets/services/image-assets.service.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// Multer writes uploads straight into the global assets folder; the service
|
|
// owns the folder location and the response record shape.
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
ensureImageAssetsDir()
|
|
.then((assetsDir) => cb(null, assetsDir))
|
|
.catch((error) => cb(error as Error, ''));
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
|
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
cb(null, `${uniqueSuffix}-${sanitizedName}`);
|
|
},
|
|
});
|
|
|
|
const upload = multer({
|
|
storage,
|
|
fileFilter: (req, file, cb) => {
|
|
if (isAllowedImageMimeType(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
|
|
}
|
|
},
|
|
limits: {
|
|
fileSize: 5 * 1024 * 1024, // 5MB
|
|
files: 5,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Stores chat image attachments in the global `~/.cloudcli/assets` folder and
|
|
* returns their absolute paths for use in provider prompts and chat history.
|
|
*/
|
|
router.post('/images', (req, res) => {
|
|
upload.array('images', 5)(req, res, (err: unknown) => {
|
|
if (err) {
|
|
const message = err instanceof Error ? err.message : 'Upload failed';
|
|
return res.status(400).json({ error: message });
|
|
}
|
|
|
|
const files = Array.isArray(req.files) ? req.files : [];
|
|
if (files.length === 0) {
|
|
return res.status(400).json({ error: 'No image files provided' });
|
|
}
|
|
|
|
res.json({ images: buildStoredImageRecords(files) });
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Serves one stored image asset by filename. Only files directly inside the
|
|
* global assets folder are reachable; traversal attempts resolve to null.
|
|
*/
|
|
router.get('/images/:filename', async (req, res) => {
|
|
const resolved = resolveImageAssetFile(req.params.filename);
|
|
if (!resolved) {
|
|
return res.status(400).json({ error: 'Invalid asset filename' });
|
|
}
|
|
|
|
try {
|
|
await fs.access(resolved);
|
|
} catch {
|
|
return res.status(404).json({ error: 'Asset not found' });
|
|
}
|
|
|
|
res.setHeader('Content-Type', mime.lookup(resolved) || 'application/octet-stream');
|
|
const fileStream = fsSync.createReadStream(resolved);
|
|
fileStream.pipe(res);
|
|
fileStream.on('error', (error) => {
|
|
console.error('Error streaming image asset:', error);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: 'Error reading asset' });
|
|
}
|
|
});
|
|
});
|
|
|
|
export default router;
|