[AWS] S3へMIMEタイプを自動判定しながらアップロードする - Node.js

AWS S3を単なるストレージとして使う際には問題とならないのですが、S3をWebサーバとして使う場合はファイルのMIMEタイプ(Content-type)を正しく指定しておかないとWebブラウザからダウンロードを求められるなど意図した通り動いてくれないことがあります。

この指定方法はシンプルでS3.putObjectを実行する際のパラメーターとして渡すだけ。

const params = {
   Bucket: 's3.example.com',
   Key: 'foo.html',
   Body: await fs.readFile('foo.html'),
   ContentType: 'text/html'
 }
 await S3.putObject(params).promise()

このときに予めMIMEタイプがすべて判明していれば良いのですが、不特定多数の種類のファイルを扱うような場合にはファイル名から拡張子を取り出して判定して……といった処理が必要になります。難しくは無いけどゼロから書くのは正直めんどくさいw

そこで今回はNode.js版のAWS SDKを用いてこのMIMEタイプを自動で判定する方法をまとめます。

※ちなみにCLIから使う場合はawsコマンド(が内部で使っているPythonのライブラリ)が自動的に判定してくれています。

基本的な原理

今回は手前味噌で恐縮ですが、自作のmimetypesjsモジュールを利用します。

www.npmjs.com

このモジュールは非常にシンプルでファイル名(xxx.txt)やファイルのパス(/home/xxx.txt)を渡すと拡張子からMIMEタイプを判定し自動で返してくれます。

const mimeTypes = require('@katsube/mimetypesjs')

const a = mimeTypes.get('a.txt')   // "text/plain"
const b = mimeTypes.get('b.html')  // "text/html"
const c = mimeTypes.get('c.js')    // "application/javascript"
const d = mimeTypes.get('d.css')   // "text/css"
const e = mimeTypes.get('e.mp4')   // "video/mp4"
const f = mimeTypes.get('f.jpg')   // "image/jpeg"
const ico = mimeTypes.get('favicon.ico')   // "image/x-icon"

// 未定義の拡張子は "application/octet-stream" が返ります
const nf = mimeTypes.get('notfound.undefined')

定義済みの拡張子の種類は以下を参照してください。この一覧の元データはApacheのmime.typesJSON化した物です。

github.com

もし一覧に存在しない場合は自分で追加することも可能です。

mimeTypes.set({ext:'wasm', type:'application/wasm'})
const w = mimeTypes.get('app.wasm')  // "application/wasm"

仕掛けがわかったという方はここで読み終えていただいて大丈夫ですw

準備

実際に稼働するプログラムを組んでいきます。

必要なモジュールをインストール

Node.js用のプロジェクトを準備していきます。yarn派の方は適宜置き換えて読んでください。

$ mkdir foo; cd foo
$ npm init

3種類のモジュールを取ってきます。

$ npm install aws-sdk dotenv @katsube/mimetypesjs

環境変数を準備

AWSのクレデンシャル情報をソースに直書きするのは現代においては御法度なので別のファイルに記録します。このファイルは絶対にGitなどのバージョン管理ツールには登録しないでください。

$ vi .env

IAMを新たに作成するなどして、IAMのアクセスキーやシークレットを以下のような書式で保存します。リージョンやバケット名はここでなくてもかまいません。

AWS_ACCESS_KEY_ID = XXXXXXXXXX
AWS_SECRET_ACCESS_KEY = XXXXXXXXXXXXXXXXXXXXXXXXXXXX
AWS_REGION = ap-northeast-1
AWS_BUCKET = s3.example.com

ソースコード

ここでは指定したファイルを1件だけアップロードするサンプルです。アップロード用のファイルがある場所は適宜変更してください。

/**
 * S3へMIMEタイプを指定してアップロード
 *
 */

//--------------------------------------------
// モジュール
//--------------------------------------------
const fs = require('fs').promises
const path = require('path')
const AWS = require('aws-sdk')
const mimetypes = require('@katsube/mimetypesjs')  // MIMEタイプ自動判定用

//--------------------------------------------
// 初期処理
//--------------------------------------------
// .envを環境変数化
require('dotenv').config()

// S3操作用のインスタンス準備
const S3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION
})

/**
 * S3へアップロード
 *
 * @param {string} file - "/var/www/html/index.html" or "index.html"
 * @return {boolean}
 */
async function UploadS3(file){
  // S3上でのファイル名(キー)
  const key = path.basename(file)  // '/home/katsube/index.html' -> 'index.html'

  // アップロード内容
  const params = {
    Bucket: process.env.AWS_BUCKET,
    Key: key,
    Body: await fs.readFile(file),
    ContentType: mimetypes.get(file),  // "text/html"
    ACL: 'public-read'  // S3でパブリックなアクセスを許可する設定が必要です
  }

  try{
    await S3.putObject(params).promise()
  }
  catch(e){
    console.error(`Upload faild ${file}: ${e.message}`)
    return(false)
  }

  console.log(`Upload success: ${key}`)
  return(true)
}

// UploadS3関数を実行する
!(async ()=>{
  await UploadS3('/home/katsube/index.html')
})();

実行する

先ほどのサンプルコードをそのまま実行するだけです。アップロードファイルの準備をお忘れなく。

$ node s3upload.js
Upload success: index.html

S3上でファイル名をクリックすると表示される画面の真ん中あたりにある「メタデータ」にContent-Typeが指定されていれば成功です。

失敗時の確認

WiFiをOFFにしたり、LANケーブルを引っこ抜いて実行すると簡単に失敗時の動作の確認ができます。

$ node s3upload.js
Upload faild /home/katsube/index.html: Inaccessible host: `s3.ap-northeast-1.amazonaws.com' at port `undefined'. This service may not be available in the `ap-northeast-1' region.

参考ページ