mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-07 06:02:43 +08:00
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { getGlobalImageAssetsDir, toPosixPath } from '@/shared/image-attachments.js';
|
|
|
|
/**
|
|
* Image mime types accepted for chat attachment uploads. SVG is allowed for
|
|
* storage/preview even though some providers (Claude API) skip it at send time.
|
|
*/
|
|
const ALLOWED_IMAGE_MIME_TYPES = new Set([
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
'image/svg+xml',
|
|
]);
|
|
|
|
// Used only by this service and the assets routes via the barrel file.
|
|
type StoredImageAsset = {
|
|
/** Original upload filename, for display. */
|
|
name: string;
|
|
/** Absolute posix-normalized path inside the global assets folder. */
|
|
path: string;
|
|
size: number;
|
|
mimeType: string;
|
|
};
|
|
|
|
// Shape of one multer-stored file; kept local because only this module reads it.
|
|
type UploadedImageFile = {
|
|
originalname: string;
|
|
filename: string;
|
|
size: number;
|
|
mimetype: string;
|
|
};
|
|
|
|
/** Returns whether one uploaded mime type may be stored as a chat image asset. */
|
|
export function isAllowedImageMimeType(mimeType: string): boolean {
|
|
return ALLOWED_IMAGE_MIME_TYPES.has(mimeType);
|
|
}
|
|
|
|
/** Creates the global `~/.cloudcli/assets` folder if needed and returns it. */
|
|
export async function ensureImageAssetsDir(): Promise<string> {
|
|
const assetsDir = getGlobalImageAssetsDir();
|
|
await fs.mkdir(assetsDir, { recursive: true });
|
|
return assetsDir;
|
|
}
|
|
|
|
/**
|
|
* Maps multer-stored upload files to the attachment records returned to the
|
|
* chat composer. The absolute path is what providers receive and what session
|
|
* history carries back to the UI.
|
|
*/
|
|
export function buildStoredImageRecords(files: UploadedImageFile[]): StoredImageAsset[] {
|
|
const assetsDir = getGlobalImageAssetsDir();
|
|
return files.map((file) => ({
|
|
name: file.originalname,
|
|
path: toPosixPath(path.join(assetsDir, file.filename)),
|
|
size: file.size,
|
|
mimeType: file.mimetype,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Resolves one asset filename to its absolute path inside the global assets
|
|
* folder, or null when the name is empty, contains path separators/traversal,
|
|
* or would escape the folder. This is the only lookup the serving route uses,
|
|
* so nothing outside `~/.cloudcli/assets` can ever be read through it.
|
|
*/
|
|
export function resolveImageAssetFile(filename: string): string | null {
|
|
const trimmed = typeof filename === 'string' ? filename.trim() : '';
|
|
if (!trimmed || trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) {
|
|
return null;
|
|
}
|
|
|
|
const assetsDir = path.resolve(getGlobalImageAssetsDir());
|
|
const resolved = path.resolve(assetsDir, trimmed);
|
|
if (!resolved.startsWith(assetsDir + path.sep)) {
|
|
return null;
|
|
}
|
|
|
|
return resolved;
|
|
}
|