Introduction
Receiving user-uploaded files through the server exhausts memory. Use S3 Presigned URLs for direct client-to-S3 uploads, scan for viruses, then serve via CDN. Generate designs with Claude Code.
CLAUDE.md File Storage Rules
## File Storage Design Rules
### Upload Method
- Client → direct S3 upload (Presigned URL)
- Don't route file transfers through the server (save memory)
- Notify server via Webhook/SQS after upload completes
### Security
- Embed file size limits in Presigned URL before upload
- Virus scan all files with ClamAV (after upload)
- Block public access until scan-complete flag is set
- Validate ContentType (prevent extension spoofing)
### Delivery
- S3 bucket is private (CloudFront only)
- Control access with CloudFront signed URLs or signed cookies
- Cache TTL: images 1 hour, documents 5 minutes
Generated File Storage Implementation
// src/storage/s3Service.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'ap-northeast-1' });
const UPLOAD_BUCKET = process.env.S3_UPLOAD_BUCKET_NAME!;
interface UploadRequest {
userId: string;
fileName: string;
contentType: string;
maxSizeBytes?: number;
}
export async function createPresignedUploadUrl(request: UploadRequest) {
const { userId, fileName, contentType, maxSizeBytes = 10 * 1024 * 1024 } = request;
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!allowedTypes.includes(contentType)) {
throw new ValidationError(`File type ${contentType} is not allowed`);
}
const extension = fileName.split('.').pop();
const fileKey = `uploads/${userId}/${crypto.randomUUID()}.${extension}`;
const command = new PutObjectCommand({
Bucket: UPLOAD_BUCKET, Key: fileKey, ContentType: contentType,
ContentLength: maxSizeBytes,
Metadata: { 'uploaded-by': userId, 'scan-status': 'pending' },
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 900 });
await prisma.fileUpload.create({
data: { fileKey, userId, fileName, contentType, status: 'pending', expiresAt: new Date(Date.now() + 900_000) },
});
return { uploadUrl, fileKey, expiresAt: new Date(Date.now() + 900_000) };
}
Virus Scanning with ClamAV
// src/storage/virusScanner.ts
export async function scanFile(fileKey: string): Promise<'clean' | 'infected'> {
const response = await s3.send(new GetObjectCommand({ Bucket: UPLOAD_BUCKET, Key: fileKey }));
const buffer = Buffer.from(await response.Body!.transformToByteArray());
const tmpPath = `/tmp/${crypto.randomUUID()}`;
await fs.writeFile(tmpPath, buffer);
try {
await execAsync(`clamscan --no-summary ${tmpPath}`);
return 'clean';
} catch (err: unknown) {
if ((err as { code: number }).code === 1) return 'infected';
throw err;
} finally {
await fs.unlink(tmpPath).catch(() => {});
}
}
export async function handleScanResult(fileKey: string, userId: string): Promise<void> {
const result = await scanFile(fileKey);
if (result === 'infected') {
await s3.send(new DeleteObjectCommand({ Bucket: UPLOAD_BUCKET, Key: fileKey }));
await prisma.fileUpload.update({ where: { fileKey }, data: { status: 'rejected', rejectedReason: 'virus_detected' } });
logger.warn({ fileKey, userId }, 'Virus detected in uploaded file');
return;
}
// Clean: copy to production bucket
const productionKey = fileKey.replace('uploads/', 'files/');
await s3.send(new CopyObjectCommand({
CopySource: `${UPLOAD_BUCKET}/${fileKey}`, Bucket: BUCKET, Key: productionKey,
MetadataDirective: 'REPLACE', Metadata: { 'scan-status': 'clean' },
}));
await prisma.fileUpload.update({ where: { fileKey }, data: { status: 'ready', productionKey } });
}
CloudFront Signed URL for Secure Delivery
// src/storage/cloudFrontService.ts
import { getSignedUrl as getCFSignedUrl } from '@aws-sdk/cloudfront-signer';
export async function getSecureFileUrl(productionKey: string, userId: string): Promise<string> {
const upload = await prisma.fileUpload.findFirst({ where: { productionKey, userId, status: 'ready' } });
if (!upload) throw new ForbiddenError('File not found or access denied');
return getCFSignedUrl({
url: `https://${process.env.CF_DOMAIN}/${productionKey}`,
keyPairId: process.env.CF_KEY_PAIR_ID!,
dateLessThan: new Date(Date.now() + 3600_000).toISOString(),
privateKey: process.env.CF_PRIVATE_KEY!,
});
}
Summary
Design S3 File Storage with Claude Code:
- CLAUDE.md — Presigned URL method, mandatory virus scanning, CloudFront delivery
- Presigned URL — direct client-to-S3 upload (no server memory used)
- ClamAV — only move clean files to production bucket after virus scan
- CloudFront signed URL — keep S3 bucket private while controlling access
Review file storage designs with **Code Review Pack (¥980)* using /code-review at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)