mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-07 06:02:43 +08:00
feat: support attached images for all providers
This commit is contained in:
93
server/modules/assets/assets.routes.ts
Normal file
93
server/modules/assets/assets.routes.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user