Node.js を CGI として動かしてファイルをレスポンスする

最近なぜかハマっている、Node.js で CGI を作るアレ。Node.js だろうと結局のところ CGI は標準入力と標準出力が使えれば何でもできる。

例えば download-file.js.cgi にアクセスして、任意の画像ファイルを表示させたければ、こんな風に書けば良い。

#!/usr/bin/node

const fs = require('fs').promises;

(async () => {
  const imgFile = await fs.readFile('./img.gif');
  
  process.stdout.write('Content-Type: image/gif\n\n');
  process.stdout.write(imgFile);
})();

ファイルを読み込んで、Content-Type ヘッダを指定して、ファイルの中身を流すだけ。

console.log() は末尾に改行コードが入ってしまうので、安全のために改行コードが入らない process.stdout.write() を使っている。

他にどんなファイルがレスポンスできるかというと、大抵のファイルはレスポンスできる。ファイルの拡張子を基に Content-Type を決めてやれば良い。

それ以外はバイナリファイルで、ブラウザ上で表示したりは出来ないだろうから、ダウンロードさせることになる。

ファイルをダウンロードさせるなら、Content-Type: application/octet-stream を指定すれば、何らかのバイナリと表現できる。

また、ダウンロードするファイルの名前を指定するなら、Content-Disposition というレスポンスヘッダを入れる。

#!/usr/bin/node

const fs = require('fs').promises;

(async () => {
  // 例えば Zip ファイルをダウンロードさせるなら
  const zipFile = await fs.readFile('./example.zip');
  
  process.stdout.write('Content-Type: application/octet-stream\n');
  process.stdout.write('Content-Disposition: attachment; filename=HOGE.zip\n\n');
  // ↑ example.zip ではなく HOGE.zip としてダウンロードさせる
  
  process.stdout.write(zipFile);
})();

ところで、冒頭のスクリプトで画像ファイルをブラウザ上で表示させた時に、その画像をダウンロードしようとすると、ファイル名が download-file.js.cgi になってしまう。

コレを解消するのも Content-Disposition が使える。attachment ではなく inline を指定すれば良い。

#!/usr/bin/node

const fs = require('fs').promises;

(async () => {
  const imgFile = await fs.readFile('./img.gif');
  
  process.stdout.write('Content-Type: image/gif\n');
  process.stdout.write('Content-Disposition: inline; filename=HOGE.gif\n\n');
  
  process.stdout.write(imgFile);
})();

コレにより、

という動きになる。

結構たやすくファイル表示・ファイルダウンロードが実装できてしまった〜。