DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Designing S3 File Storage with Claude Code: Presigned URLs, Virus Scanning, CDN Delivery

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
Enter fullscreen mode Exit fullscreen mode

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) };
}
Enter fullscreen mode Exit fullscreen mode

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 } });
}
Enter fullscreen mode Exit fullscreen mode

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!,
  });
}
Enter fullscreen mode Exit fullscreen mode

Summary

Design S3 File Storage with Claude Code:

  1. CLAUDE.md — Presigned URL method, mandatory virus scanning, CloudFront delivery
  2. Presigned URL — direct client-to-S3 upload (no server memory used)
  3. ClamAV — only move clean files to production bucket after virus scan
  4. 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)