import { ConfigService } from '@nestjs/config'; import * as path from 'path'; import * as fs from 'fs'; import * as crypto from 'crypto'; import multer from 'multer'; export interface UploadLimits { fileSize: number; } /** * Build a multer-style `limits` object from the UPLOAD_SIZE_LIMIT env value. * Examples: '50mb', '500kb', '2gb'. Defaults to '50mb'. */ export function parseUploadSizeLimit(value: string | undefined): number { const v = (value ?? '50mb').trim().toLowerCase(); const m = /^(\d+(?:\.\d+)?)(b|kb|mb|gb)$/.exec(v); if (!m) return 50 * 1024 * 1024; const n = parseFloat(m[1]); const unit = m[2]; const mult = unit === 'b' ? 1 : unit === 'kb' ? 1024 : unit === 'mb' ? 1024 * 1024 : 1024 * 1024 * 1024; return Math.floor(n * mult); } export function buildUploadLimits(config: ConfigService): UploadLimits { const v = config.get('UPLOAD_SIZE_LIMIT', '50mb'); return { fileSize: parseUploadSizeLimit(v) }; } /** * Produce a safe, collision-resistant filename from the original upload * filename. Strips directory traversal, lowercases, restricts to a safe * charset, caps the stem length, and appends a short random suffix so * identical names from different uploads never overwrite each other. */ export function safeFilename(original: string): string { const base = path.basename(original || 'file').toLowerCase(); const cleaned = base .replace(/[^a-z0-9._-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 120); const stem = cleaned || 'file'; const dot = stem.lastIndexOf('.'); const head = dot > 0 ? stem.slice(0, dot) : stem; const ext = dot > 0 ? stem.slice(dot) : ''; const suffix = crypto.randomBytes(4).toString('hex'); const safeHead = (head || 'file').slice(0, 100); return `${safeHead}-${suffix}${ext}`; } /** * Build a configured Multer instance with a per-route `fileSize` limit * (taken from `UPLOAD_SIZE_LIMIT`) and safe-filename storage at the * supplied destination directory. */ export interface MulterOptions { destination?: string; fieldName?: string; fileSize?: number; } export function buildMulter(config: ConfigService, opts: MulterOptions = {}): multer.Multer { const limits = opts.fileSize !== undefined ? { fileSize: opts.fileSize } : buildUploadLimits(config); const dest = opts.destination ?? path.resolve(config.get('UPLOAD_DIR', '/data/hipctf/uploads')); if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); return multer({ storage: multer.diskStorage({ destination: (_req, _file, cb) => cb(null, dest), filename: (_req, file, cb) => cb(null, safeFilename(file.originalname)), }), limits, }); }