Files
claudecodeui/server/modules/assets/services/image-assets.service.ts
2026-07-06 13:32:06 +03:00

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;
}