Blob StorageにPDFをアップロードしたらContentTypeを確認しよう【自戒記事】

## 先に結論
- Azure SDK を用いてPDFファイルをアップロードした場合、**ContentTypeのデフォルト値は`application/octet-stream`になる模様**
- ContentTypeが`application/octet-stream`のBlobの場合、WEBブラウザから当該BlobのURLにアクセスすると、プレビューではなくファイルダウンロードに遷移してしまう
  - → **ContentTypeを`application/pdf`に変えればプレビュー可能**
  - この差分は、ブラウザ側で参照データのContentTypeを元に振る舞いを制御していることに起因する
- `@azure/storage-blob`などのAzure Storage クライアントライブラリを用いてファイルをアップロードしている場合、Blobアップロード関数のOptionで `blobHTTPHeaders` の `blobContentType`を設定することで任意のContentTypeを指定できる

## 検証条件
- 言語：TypeScript
- ランタイム：Node.js
- Azure Storageクライアントライブラリ：@azure/storage-blob

## やりたいこと
1. WEBアプリからPDFファイルをBlob Storageにアップロードしたい
2. アップロードしたPDFファイルのURLを取得し、WEBアプリの任意のボタンを押すと当該ファイルをWEBブラウザ上で開くようにしたい

## やったこと：PDFファイルのアップロード（修正前）

https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-upload-javascript?tabs=javascript

上記のMS Learnを参考に、`BlockBlobClient`を生成し、`uploadStream`関数を用いてPDFファイル（Readableストリームに変換済み）をBlob Storageにアップロードする。

```.ts
const uploadDataAsPdf: (data: File) => Promise<void> = async (data) => {
  /** アップロード先 Azure Blob Storage コンテナ名 */
  const containerName = "upload-test";
  /** アップロードするPDFファイル名 */
  const blobName = "upload-test.pdf";

  // File型の data をストリームに変換
  const fileStream = data.stream();
  const reader = fileStream.getReader();
  /** アップロードするPDFファイルデータ */
  const targetData: Readable = new Readable({
    async read() {
      const result = await reader.read();
      if (result.done) {
        this.push(null);
      } else {
        this.push(Buffer.from(result.value));
      }
    },
  });
  
  /** アップロード先 Azure Blob Storage の接続文字列 */
  const clientUrl = 'hogehogehogehoge';
  /** 
   * Blob サービスクライアントを作成
   * @see https://learn.microsoft.com/ja-jp/javascript/api/overview/azure/storage-blob-readme?view=azure-node-latest#using-connection-string
   */
  const blobServiceClient = BlobServiceClient.fromConnectionString(clientUrl);
  /** コンテナクライアントインスタンス */
  const containerClient = blobServiceClient.getContainerClient(containerName);
  /** Block Blobクライアント */
  const blockBlobClient = containerClient.getBlockBlobClient(blobName);
  // Blob StorageにPDFファイル（Readbaleストリーム）をアップロード
  await blockBlobClient.uploadStream(stream);
}
```
この関数を用いて、リクエストボディで受け取ったFileデータをAzure Blob StorageにアップロードするPOST APIを作成。
お試ししてみると……

↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/b67aa1c6-fb69-4cc5-944a-60250e59c479.png)
アップロードに成功した！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/3a45cac4-067a-43be-b572-e17ae25b84a2.png)
プレビューも確認したが、問題なさそうだ！

ということでやりたいこと1「WEBアプリからPDFファイルをBlob Storageにアップロードしたい」はクリアー。


ではアップロードしたPDFファイルのURLにブラウザからアクセスしてみよう！

↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/1896c8d0-2189-46e1-886c-8bbd8955303a.png)

**ダウンロードポップアップが開いた？？？？？**

やりたいこと2は「アップロードしたPDFファイルのURLを取得し、WEBアプリの任意のボタンを押すと当該ファイルをWEBブラウザ上で開く」。このままではWEBブラウザ上でのプレビューが実現できない！


## よわよわエンジニア、ContentType（MIMEタイプ）を知る
よわよわエンジニアがあれこれネットの海に問い合わせたところ、どうやらブラウザでPDFをプレビューするには**ContentTypeを`application/pdf`にする**必要があるっぽいことを発見。

ContentTypeは、ファイルやインターネット上で転送されるデータの形式を識別するためのコード。
MIMEタイプとも表記される。
よくAPIレスポンスなどで目にする `text/plain`や`application/json`、`image/jpeg`などもContentTypeの一種。

https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/MIME_types/Common_types


今回はFile（＝バイナリデータ）をアップロードしたが、この場合のContentTypeのデフォルト値は`application/octet-stream`になる模様。拡張子で言うと`.bin`が相当。任意の種類のバイナリーデータならなんでも`application/octet-stream`。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/9694a05a-27c3-4396-9679-273dee5973ef.png)

↑たしかに upload-test.pdf のContentTypeは`application/octet-stream`である……。

そして、「ダウンロードポップアップの表示」は、**参照したデータのContentTypeが`application/octet-stream`の場合のブラウザ側の挙動**だった。

もしデータの**ContentTypeが`application/pdf`になっている**と、**ブラウザ側がPDFファイルに対するデフォルトの振る舞いをしてくれる**。それが今回の期待値である「PDFのブラウザ上でのプレビュー」だったというわけである。

## やったこと：PDFファイルのアップロード（修正後）
ということで、Blobストレージにアップロードする際、明示的に「このBlobはPDFなんですよ！」と示してあげれば良いと判明。

今回用いていた Azure Storageクライアントライブラリ`@azure/storage-blob`の `uploadStream`関数のリファレンスを確認する。

https://learn.microsoft.com/ja-jp/javascript/api/%40azure/storage-blob/blockblobclient?view=azure-node-latest#@azure-storage-blob-blockblobclient-uploadstream

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/76553c23-1605-4c2e-8fd7-8b82866d4992.png)

第4引数`options`に指定する`BlockBlobUploadStreamOptions`の詳細を確認すると、この中に`blobHTTPHeaders`という、`blobContentType`とやらを設定するプロパティがあることがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/301bbcf3-b6c1-4aca-b896-0f724ca3daf8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/7c3f69c3-b1dd-4ba0-bd76-4024a91cd179.png)

ということで、先ほどのソースのうち`uploadStream`関数を呼び出していた部分を以下の通り修正。

``` diff_typescript
// Blob StorageにPDFファイル（Readbaleストリーム）をアップロード
- await blockBlobClient.uploadStream(stream);
+ await blockBlobClient.uploadStream(
+   stream,
+   undefined, // bufferSize :デフォルト値「8」を使用
+   undefined, // maxConcurrency :デフォルト値「5」を使用
+   {
+     blobHTTPHeaders: { blobContentType: "application/pdf" }, // PDFとしてアップロード
+   },
+ );
```

これで再度試してみると……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/fc37c02d-6049-4b12-a64b-5782a3cdc6cb.png)

**↑ upload-test.pdf のContentTypeが`application/pdf`になっている！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659383/65a2cee7-2430-403e-8c9d-250c9e6b830a.png)

**↑ URLにアクセスすると、ちゃんとPDFをプレビューできている！**


## 結論
Azure Blob StorageのようなBlobサービスを用いる際は、当該サービスの概念（コンテナとか仮想ディレクトリとか）を押さえることも、Azure SDKを使えることも大事であるが、
まずは扱うデータの特性や仕様自体をちゃんと押さえる必要がある（当たり前体操）

動いたやったね！！！ で終わらず、「で、私いまPDFファイルを何をどうしてStorageにアップロードできたの？？？ FileってなにBlobってなに？？？」……など自分が書いたコード/実施した操作の内容をちゃんと把握しよう。。