mcpField
F
file-upload

file-upload

github.com/claude-dev-suite開発4636 回インストール3 日前に更新
リポジトリ

file-upload について

Node.js、Python、Java サービスでのファイルアップロードを扱う開発資料です。保存方法、検証、サイズ制限、本番環境での対策を説明します。

主な機能

  1. 01Multer のディスク・メモリ保存例
  2. 02FastAPI と Spring Boot のアップロード手法
  3. 03マジックバイトによるファイル形式検証
  4. 04アップロードサイズ制限と安全なファイル名
  5. 05アップロードエンドポイントの本番チェックリスト

活用シーン

  1. 01Node.js サービスでアップロードを実装
  2. 02MIME 表記だけに頼らずファイル内容を検証
  3. 03ユーザー向けアップロードエンドポイントの防御設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill file-upload

インストール後、Claude Code で/file-uploadを入力して読み込まれたか確認します。

詳細

ファイルアップロードの処理

Node.js(Multer — 推奨)

import multer from 'multer';
import path from 'path';
import crypto from 'crypto';

// Storage configuration
const storage = multer.diskStorage({
  destination: './uploads',
  filename: (req, file, cb) => {
    const uniqueName = `${crypto.randomUUID()}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  },
});

// File filter
const fileFilter = (req: Express.Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
  const allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (allowedMimes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error(`File type ${file.mimetype} not allowed`));
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
});

// Single file
app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ filename: req.file!.filename, size: req.file!.size });
});

// Multiple files
app.post('/upload/multiple', upload.array('files', 10), (req, res) => {
  res.json({ count: (req.files as Express.Multer.File[]).length });
});

// Error handling
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') return res.status(413).json({ error: 'File too large' });
    return res.status(400).json({ error: err.message });
  }
  if (err.message.includes('not allowed')) return res.status(415).json({ error: err.message });
  next(err);
});
メモリストレージ(クラウドへの転送用)
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });

app.post('/upload', upload.single('file'), async (req, res) => {
  // req.file.buffer contains the file — forward to S3
  await s3.send(new PutObjectCommand({
    Bucket: bucket, Key: key, Body: req.file!.buffer, ContentType: req.file!.mimetype,
  }));
});

Python(FastAPI)

from fastapi import UploadFile, File, HTTPException
import aiofiles, uuid

ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE = 10 * 1024 * 1024  # 10MB

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    if file.content_type not in ALLOWED_TYPES:
        raise HTTPException(415, f"Type {file.content_type} not allowed")

    content = await file.read()
    if len(content) > MAX_SIZE:
        raise HTTPException(413, "File too large")

    filename = f"{uuid.uuid4()}{Path(file.filename).suffix}"
    async with aiofiles.open(f"uploads/{filename}", "wb") as f:
        await f.write(content)

    return {"filename": filename, "size": len(content)}

Java(Spring Boot)

@PostMapping("/upload")
public ResponseEntity<Map<String, String>> upload(
        @RequestParam("file") MultipartFile file) {

    if (file.isEmpty()) throw new ResponseStatusException(BAD_REQUEST, "Empty file");
    if (file.getSize() > 10_000_000) throw new ResponseStatusException(PAYLOAD_TOO_LARGE);

    String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());
    String filename = UUID.randomUUID() + "." + ext;
    Path dest = Path.of("uploads", filename);
    file.transferTo(dest);

    return ResponseEntity.ok(Map.of("filename", filename));
}

Spring の設定:

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

MIME タイプ以外のファイル検証

// Validate actual file content (magic bytes), not just extension
import { fileTypeFromBuffer } from 'file-type';

const type = await fileTypeFromBuffer(req.file!.buffer);
if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
  return res.status(415).json({ error: 'Invalid file content' });
}

アンチパターン

アンチパターン修正方法
クライアントの MIME タイプのみを信頼するfile-type でマジックバイトを検証する
元のファイル名をストレージキーとして使用するパストラバーサルや衝突を防ぐため、UUID を使用する
ファイルサイズの上限を設けない必ず limits.fileSize を設定する
アップロード時に同期的にディスクへ書き込むストリームまたは非同期書き込みを使用する
アプリケーションディレクトリにアップロードファイルを保存する/uploads を別途用意するか、クラウドストレージを使用する
一時ファイルをクリーンアップしないライフサイクルまたは cron によるクリーンアップを実装する

本番環境チェックリスト

  • ファイルサイズの上限が設定されている
  • MIME タイプの許可リストが設定されている(拡張子だけでなく、マジックバイトを検証する)
  • ファイル名に UUID を使用している(保存時に元のファイル名を決して使用しない)
  • Multer のエラーハンドラーミドルウェアがある
  • ユーザーのアップロードファイルをウイルススキャンしている(ClamAV)
  • アップロードエンドポイントにレート制限を設定している
  • 孤立したファイルのクリーンアップ戦略がある

コメント