Back

Multer NPM: Node.jsでのファイルアップロード

Multer NPM: Node.jsでのファイルアップロード

ファイルアップロードはWebアプリケーションでよくある要件ですが、Node.jsで適切に処理するのは難しい場合があります。MulterはExpress.js用のミドルウェアで、ファイルアップロードを簡単かつ効率的に行うことができます。この記事では、Node.jsアプリケーションでファイルアップロードを処理するためのMulterの使用方法について説明します。

重要ポイント

  • Multerはmultipart/form-dataを処理することでNode.jsでのファイルアップロードを簡素化
  • ニーズに基づいたストレージオプションの設定(ディスク、メモリ、クラウド)
  • セキュリティを確保するための適切な検証とフィルタリングの実装
  • 乱用を防ぐための適切な制限の設定
  • 良好なユーザー体験を提供するためのエラーの適切な処理
  • 本番アプリケーションにはクラウドストレージの検討

Multerとは?

MulterはHTMLフォームを通じてファイルをアップロードする際に使用されるmultipart/form-data形式を処理するために特別に設計されたNode.jsミドルウェアです。busboyの上に構築されており、Multerは効率的にファイルアップロードを処理し、Expressルート内でアクセス可能にします。

Multerの主な機能には以下が含まれます:

  • 単一および複数のファイルアップロードの処理
  • 設定可能なストレージオプション
  • ファイルフィルタリング機能
  • サイズ制限と検証
  • Express.jsとのシームレスな統合

Multipart Form Dataの理解

Multerに深く入る前に、通常のフォーム処理がファイルアップロードに適用できない理由を理解することが重要です。

フォームにファイルが含まれる場合、ブラウザはデフォルトのapplication/x-www-form-urlencodedではなくmultipart/form-dataとしてエンコードします。このエンコーディングにより、バイナリデータ(ファイル)をテキストフィールドと一緒に送信することができます。

Expressの組み込みミドルウェア(express.json()express.urlencoded())はマルチパートデータを処理できないため、Multerが必要になります。

<!-- このフォームはmultipart/form-dataを送信します -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
  <input type=""file"" name=""document"">
  <button type=""submit"">Upload</button>
</form>

Multerの使用開始

インストール

まず、Node.jsプロジェクトにMulterをインストールします:

npm install multer

基本設定

ExpressアプリケーションでのシンプルなMulterの設定例:

const express = require('express');
const multer = require('multer');

const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('document'), (req, res) => {
  // req.fileにはアップロードされたファイルに関する情報が含まれています
  console.log(req.file);
  
  // req.bodyにはテキストフィールドが含まれています
  console.log(req.body);
  
  res.send('File uploaded successfully');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

この例では:

  1. アップロード用の保存先フォルダを指定してMulterインスタンスを作成
  2. 単一ファイルアップロードを処理するためにupload.single('document')ミドルウェアを使用
  3. アップロードされたファイル情報はreq.fileで利用可能

様々なアップロードシナリオの処理

Multerは様々なファイルアップロードシナリオを処理するためのいくつかのメソッドを提供しています:

単一ファイルアップロード

// フィールド名が'profile'の単一ファイルを処理
app.post('/profile', upload.single('profile'), (req, res) => {
  // req.fileにはアップロードされたファイルが含まれています
  res.json({ 
    message: 'File uploaded successfully',
    file: req.file 
  });
});

複数ファイル(同じフィールド)

// フィールド名が'photos'の複数ファイル(最大5つ)を処理
app.post('/photos', upload.array('photos', 5), (req, res) => {
  // req.filesにはファイルの配列が含まれています
  res.json({ 
    message: `${req.files.length} files uploaded successfully`,
    files: req.files 
  });
});

複数ファイル(異なるフィールド)

const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 3 }
]);

app.post('/profile', uploadFields, (req, res) => {
  // req.filesはフィールド名をキーとするオブジェクト
  // req.files.avatarにはアバターファイルが含まれています
  // req.files.galleryにはギャラリーファイルが含まれています
  res.json({ 
    message: 'Files uploaded successfully',
    avatar: req.files.avatar,
    gallery: req.files.gallery
  });
});

テキストのみのマルチパートフォーム

app.post('/form-data', upload.none(), (req, res) => {
  // テキストフィールドのみを処理し、ファイルは拒否します
  res.json(req.body);
});

ストレージオプション

Multerはファイルの保存場所と方法を制御するための異なるストレージエンジンを提供しています。

ディスクストレージ

ファイルストレージをより詳細に制御するには、diskStorageを使用します:

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // ユニークなファイル名を作成
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + getExtension(file.originalname));
  }
});

function getExtension(filename) {
  return filename.substring(filename.lastIndexOf('.'));
}

const upload = multer({ storage: storage });

この設定により以下を制御できます:

  • ファイルの保存場所(destination)
  • ファイル名(filename)

メモリストレージ

ディスクに保存せずにメモリ内でファイルを処理する必要がある場合:

const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

app.post('/upload', upload.single('file'), (req, res) => {
  // req.file.bufferにはファイルデータが含まれています
  // メモリ内でファイルを処理(例:クラウドストレージにアップロード)
  
  // 例:ファイルバッファとタイプを取得
  const fileBuffer = req.file.buffer;
  const fileType = req.file.mimetype;
  
  res.send('File processed');
});

メモリストレージは以下の場合に便利です:

  • ファイルを別のサービス(S3やCloudinaryなど)に渡す場合
  • 保存前にファイルを処理する必要がある場合
  • サーバーレス環境で作業している場合

ファイルフィルタリング

fileFilterオプションを使用して、受け入れるファイルを制御できます:

const fileFilter = (req, file, cb) => {
  // 画像ファイルのみを受け入れる
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Only image files are allowed!'), false);
  }
};

const upload = multer({ 
  storage: multer.diskStorage({...}),
  fileFilter: fileFilter
});

サイズ制限とセキュリティ

セキュリティとパフォーマンスのために制限を設定することが重要です:

const upload = multer({
  storage: multer.diskStorage({...}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MBをバイト単位で指定
    files: 5 // リクエストあたり最大5ファイル
  }
});

その他の制限オプションには以下が含まれます:

  • fieldNameSize:最大フィールド名サイズ(デフォルト100バイト)
  • fieldSize:最大フィールド値サイズ(デフォルト1MB)
  • fields:非ファイルフィールドの最大数(デフォルトInfinity)
  • parts:パーツ(フィールド+ファイル)の最大数(デフォルトInfinity)

エラー処理

Multerのエラーは意味のあるフィードバックを提供するために適切に処理する必要があります:

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Multerエラーが発生
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large' });
      }
      return res.status(400).json({ error: err.message });
    } else if (err) {
      // 不明なエラーが発生
      return res.status(500).json({ error: 'Server error' });
    }
    
    // すべて正常
    res.json({ message: 'File uploaded successfully', file: req.file });
  });
});

完全な例:ファイルアップロードシステムの構築

フロントエンドとバックエンドを備えた完全なファイルアップロードシステムを構築しましょう:

バックエンド(server.js)

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();
const port = 3000;

// 静的ファイルの提供
app.use(express.static('public'));

// ストレージの設定
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    // ディレクトリが存在しない場合は作成
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

// ファイルフィルターの設定
const fileFilter = (req, file, cb) => {
  // 画像とPDFを受け入れる
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'), false);
  }
};

// Multerの設定
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5
  }
});

// 単一ファイルアップロードルート
app.post('/upload/single', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.file) {
      return res.status(400).json({ success: false, message: 'No file provided' });
    }
    
    res.json({
      success: true,
      message: 'File uploaded successfully',
      file: {
        filename: req.file.filename,
        originalname: req.file.originalname,
        mimetype: req.file.mimetype,
        size: req.file.size,
        path: req.file.path
      }
    });
  });
});

// 複数ファイルアップロードルート
app.post('/upload/multiple', (req, res) => {
  upload.array('files', 5)(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ success: false, message: 'No files provided' });
    }
    
    const fileDetails = req.files.map(file => ({
      filename: file.filename,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size,
      path: file.path
    }));
    
    res.json({
      success: true,
      message: `${req.files.length} files uploaded successfully`,
      files: fileDetails
    });
  });
});

// アップロードされたファイルのリストを取得
app.get('/files', (req, res) => {
  const uploadDir = 'uploads';
  
  fs.readdir(uploadDir, (err, files) => {
    if (err) {
      return res.status(500).json({ success: false, message: 'Error reading files directory' });
    }
    
    res.json({
      success: true,
      files: files
    });
  });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

フロントエンド(public/index.html)

<!DOCTYPE html>
<html lang=""en"">
<head>
  <meta charset=""UTF-8"">
  <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
  <title>File Upload with Multer</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .upload-section {
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    .file-list {
      margin-top: 20px;
    }
    .file-item {
      padding: 10px;
      margin: 5px 0;
      background-color: #f5f5f5;
      border-radius: 3px;
    }
    .error {
      color: red;
      margin-top: 10px;
    }
    .success {
      color: green;
      margin-top: 10px;
    }
    progress {
      width: 100%;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>File Upload with Multer</h1>
  
  <div class=""upload-section"">
    <h2>Single File Upload</h2>
    <form id=""singleUploadForm"">
      <input type=""file"" id=""singleFile"" name=""file"" required>
      <button type=""submit"">Upload File</button>
    </form>
    <div id=""singleUploadResult""></div>
    <progress id=""singleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Multiple File Upload</h2>
    <form id=""multipleUploadForm"">
      <input type=""file"" id=""multipleFiles"" name=""files"" multiple required>
      <button type=""submit"">Upload Files</button>
    </form>
    <div id=""multipleUploadResult""></div>
    <progress id=""multipleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Uploaded Files</h2>
    <button id=""refreshFiles"">Refresh List</button>
    <div id=""fileList"" class=""file-list""></div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // Single file upload
      const singleUploadForm = document.getElementById('singleUploadForm');
      const singleUploadResult = document.getElementById('singleUploadResult');
      const singleUploadProgress = document.getElementById('singleUploadProgress');
      
      singleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('singleFile');
        
        if (!fileInput.files.length) {
          singleUploadResult.innerHTML = '<div class=""error"">Please select a file</div>';
          return;
        }
        
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        
        singleUploadProgress.style.display = 'block';
        singleUploadProgress.value = 0;
        singleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            singleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            singleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            singleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            singleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          singleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/single', true);
        xhr.send(formData);
      });
      
      // Multiple file upload
      const multipleUploadForm = document.getElementById('multipleUploadForm');
      const multipleUploadResult = document.getElementById('multipleUploadResult');
      const multipleUploadProgress = document.getElementById('multipleUploadProgress');
      
      multipleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('multipleFiles');
        
        if (!fileInput.files.length) {
          multipleUploadResult.innerHTML = '<div class=""error"">Please select at least one file</div>';
          return;
        }
        
        const formData = new FormData();
        for (let i = 0; i < fileInput.files.length; i++) {
          formData.append('files', fileInput.files[i]);
        }
        
        multipleUploadProgress.style.display = 'block';
        multipleUploadProgress.value = 0;
        multipleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            multipleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            multipleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            multipleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            multipleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          multipleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/multiple', true);
        xhr.send(formData);
      });
      
      // Load files
      const fileList = document.getElementById('fileList');
      const refreshFilesButton = document.getElementById('refreshFiles');
      
      function loadFiles() {
        fetch('/files')
          .then(response => response.json())
          .then(data => {
            if (data.success) {
              if (data.files.length === 0) {
                fileList.innerHTML = '<p>No files uploaded yet</p>';
              } else {
                let html = '';
                data.files.forEach(file => {
                  html += `<div class=""file-item"">${file}</div>`;
                });
                fileList.innerHTML = html;
              }
            } else {
              fileList.innerHTML = `<div class=""error"">${data.message}</div>`;
            }
          })
          .catch(error => {
            fileList.innerHTML = '<div class=""error"">Error loading files</div>';
          });
      }
      
      refreshFilesButton.addEventListener('click', loadFiles);
      
      // Initial load
      loadFiles();
    });
  </script>
</body>
</html>

クラウドストレージとの統合

本番アプリケーションでは、サーバー上ではなくクラウドストレージにファイルを保存することが多いです。以下はMulterとAWS S3を統合する方法です:

const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

// AWSの設定
AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION
});

const s3 = new AWS.S3();

// S3を使用するようにMulterを設定
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: process.env.S3_BUCKET_NAME,
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
      cb(null, uniqueName);
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({
    success: true,
    message: 'File uploaded to S3 successfully',
    fileLocation: req.file.location // S3のURL
  });
});

注意:multer-s3パッケージをインストールする必要があります:npm install multer-s3 aws-sdk

セキュリティのベストプラクティス

ファイルアップロードを実装する際は、以下のセキュリティのベストプラクティスに従ってください:

  1. ファイルタイプの検証:常にMIMEタイプと拡張子を確認する
  2. ファイルサイズの制限:大きなファイルアップロードによるDoS攻撃を防止する
  3. マルウェアスキャン:アップロードされたファイルのウイルススキャンの統合を検討する
  4. 安全なストレージの使用:公開ディレクトリにファイルを保存しない
  5. ランダムなファイル名の生成:ユーザー提供のファイル名を直接使用しない
  6. 認証の実装:認証されたユーザーのみがファイルをアップロードできるようにする
  7. アップロード制限の設定:リクエストごとおよびユーザーごとのファイル数を制限する

結論

MulterはNode.jsアプリケーションでのファイルアップロード処理に堅牢なソリューションを提供します。この記事で説明したパターンとプラクティスに従うことで、安全で効率的、そしてユーザーフレンドリーなファイルアップロード機能を実装することができます。シンプルな画像アップローダーから複雑な文書管理システムまで、MulterのフレキシブルなAPIと統合機能は、Expressアプリケーションでmultipart/form-dataを処理するための優れた選択肢です。

よくある質問

大きなファイルの場合、以下を検討してください:Multer設定でサイズ制限を増やす、非常に大きなファイルの場合はチャンク分割アップロードを実装する、メモリに完全に読み込まずにファイルを処理するためのストリーミングを使用する、フロントエンドで進捗状況の追跡を実装する。

はい、MulterはTypeScriptとうまく連携します。以下のようにして型定義をインストールしてください:npm install @types/multer

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers