Azure Storage、マルチパートでアップロードできないってよ

# 概要

Azure Storage(Blob)へファイルをアップロードする際は、マルチパートが使えない模様。困った。

Blob APIを使う必要があるらしいが[PUTの制限](https://docs.microsoft.com/en-us/rest/api/storageservices/Put-Blob?redirectedfrom=MSDN
)があるため、
256MB以上のファイルはブロックに分割してアップロードする必要がある模様。

しかし自分で実装するとなると、これがそこそこ面倒。困った。

そこで、ブラウザから(JavaScriptで)分割してアップロードする方法を調べていたところ、
[公式のライブラリ](https://azure.microsoft.com/ja-jp/blog/new-azure-storage-javascript-client-library-for-browsers-preview/)があるようなので、実際に試してみた。 

結論から言うと、ライブラリを利用することで、分割アップロードを簡単に実現することができた。
Azure Storage(Blob)へのファイルアップロードはファイルサイズに関係なく、
今回試した方法を使うで良さそう。

※ 試したコードはgithubでも公開中 [azure-storage-file-uploader](https://github.com/mochi8k/azure-storage-file-uploader)

# デモ
![azure-storage-upload-download.gif](https://qiita-image-store.s3.amazonaws.com/0/74485/dd2f2575-8b54-f5e1-5a46-0153c3362ff0.gif)
※ 256MB以上のファイルをアップロードすると、アップロード時間が長いので1MBのファイルでのデモ

# 手順

## Azure Storageの準備

　[1. ストレージアカウントの作成](#1-ストレージアカウントの作成)
　[2. CORSの設定](#2-corsの設定)
　[3. SASトークンの生成](#3-sasトークンの生成)

### 1. ストレージアカウントの作成
[公式の手順](https://docs.microsoft.com/ja-jp/azure/storage/common/storage-create-storage-account#create-a-storage-account
)を参考に作成するだけ
※ ストレージの種類は今回のターゲットの「BLOB ストレージ」を選択

### 2. CORSの設定
サイドバーから「CORS」を選択し、以下のように設定。

```
許可されたオリジン: *
許可されたメソッド: GET,HEAD,OPTIONS,PUT,POST
許可されたヘッダー: Accept,Content-Type,Origin,x-ms-blob-content-disposition,x-ms-blob-content-md5,x-ms-blob-content-type,x-ms-blob-type,x-ms-client-request-id,x-ms-date,x-ms-version
公開されるヘッダー: *
最大期間(秒): 0
```
※ 許可されたヘッダーに関しては「*」ではなく調査しがてら明示的に必要なヘッダーを指定した。

### 3. SASトークンの生成
サイドバーから「Shared Access Signature」を選択。

お試し程度であれば、基本デフォルト値で問題ないが、
「開始日時と有効期限の日時」のタイムゾーンは「UTC +09:00」に変更することをお忘れなく。
最後に「SASの生成」を押下し、生成された「SAS トークン」をメモっておく。

これでAzure Storageの準備は完了。

## ファイルアップロードの実装

　[1. ライブラリの導入](#1-ライブラリの導入)
　[2. 必要な情報の定数化](#2-必要な情報の定数化)
　[3. コンテナの生成](#3-コンテナの生成)
　[4. アップロード](#4-アップロード)
　[5. プログレス表示](#5-プログレス表示)
　[6. ダウンロード](#6-ダウンロード)

最終的なコードはこんな感じ。ライブラリの[サンプルコード](https://dmrelease.blob.core.windows.net/azurestoragejssample/samples/sample-blob.html)を参考にしつつ進めた。
せっかくなので、アップロードだけでなく、プログレスバーやダウンロードも入れてみた。


```html:index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Azure Storage File Uploader</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
    </head>
    <body>
        <div><img id="image" src=""/></div>
        <input id="inputFile" type="file" />
        <i id="download" class="fa fa-download" aria-hidden="true"></i>
        <div><progress id="progress" max="100"></progress></div>
        <style>
         html, body {
             width: 100%;
             height: 100%;
             margin: 0;
             padding: 0;
         }
         html {
             display: table;
         }
         body {
             display: table-cell;
             text-align: center;
             vertical-align: middle;
         }
         #image{
             width: 300px;
         }
         #progress {
             display: none;
             margin-left: -70px;
         }
         #download {
             display: none;
             cursor: pointer;
         }
        </style>
        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
        <script src="azure-storage.common.min.js"></script>
        <script src="azure-storage.blob.min.js"></script>
        <script src="main.js"></script>
    </body>
</html>
```

```js:main.js
const SAS_TOKEN = 'sas-token';
const CONTAINER_NAME = 'my-container-name';
const BLOB_URI = 'https://my-storage-name.blob.core.windows.net';

const blobService = AzureStorage.createBlobServiceWithSas(BLOB_URI, SAS_TOKEN);

$('#inputFile').on('change', (event) => {
    blobService.createContainerIfNotExists(CONTAINER_NAME, (error, result) => {
        if (error) {
            console.error('create container error');
            return;
        }
        
        const file = event.target.files[0];
        const customBlockSize = (file.size > 1024 * 1024 * 32)? (1024 * 1024 * 4) : (1024 * 512);
        blobService.singleBlobPutThresholdInBytes = customBlockSize;

        const options = {
            blockSize: customBlockSize,
            contentSettings: {
                contentDisposition: 'attachment'
            }
        };

        beforeUpload();
        
        let finishedOrError = false;
        const speedSummary = blobService.createBlockBlobFromBrowserFile(CONTAINER_NAME, file.name, file, options, (error, result, response) => {
            finishedOrError = true;
            if (error) {
                console.error('upload error');
                return;
            }
            console.log('upload successfully');
            afterUpload();
        });
        
        function refreshProgress() {
            setTimeout(() => {
                if (!finishedOrError) {
                    $('#progress').val(speedSummary.getCompletePercent());
                    refreshProgress();
                }
            }, 200);
        }
        
        refreshProgress();    
    });
    
});

$('#download').on('click', (event) => {
    const file = $('#inputFile').prop('files').item(0);
    if (file == null) return;
    const downloadUrl = blobService.getUrl(CONTAINER_NAME, file.name, SAS_TOKEN);
    $('#image').attr('src', downloadUrl);
});

function beforeUpload() {
    $('#progress').val(0);
    $('#progress').show();
    $('#download').hide();
}

function afterUpload() {
    $('#progress').hide();
    $('#download').show();
}
```
### 1. ライブラリの導入
[README](https://github.com/Azure/azure-storage-node/blob/master/browser/README.md)どおりに進めればOK。
生成された以下の2ファイルをindex.htmlで読み込む。

- azure-storage.common.min.js
- azure-storage.blob.min.js

### 2. 必要な情報の定数化

以下の3つがあればOK。
SASトークン: 生成したSASトークン 　※ "?"以降の値を使用する
コンテナ名: 任意のコンテナ名
Blob URI: https:{ストレージ名}.blob.core.windows.net

```js
const SAS_TOKEN = 'sas-token';
const CONTAINER_NAME = 'my-container-name';
const BLOB_URI = 'https://my-storage-name.blob.core.windows.net';
```

### 3. コンテナの生成
任意のコンテナ名を引数とすることで、コンテナが生成される。
既に同名のコンテナが存在する場合は、再生成されることはないので、必ず実行して問題ない。

```js
blobService.createContainerIfNotExists(CONTAINER_NAME, (error, result) => {
```
ドキュメントは[こちら](https://azure.github.io/azure-storage-node/BlobService.html#createContainerIfNotExists__anchor)

### 4. アップロード

コンテナ名、ファイル情報、オプションを指定してファイルをアップロードする。
ファイルサイズに応じたブロックに分割して、リクエストが送信される。

```js
const file = event.target.files[0];
const customBlockSize = (file.size > 1024 * 1024 * 32)? (1024 * 1024 * 4) : (1024 * 512); // ファイルサイズに応じてブロックサイズを決定
blobService.singleBlobPutThresholdInBytes = customBlockSize;

const options = {
    blockSize: customBlockSize,
    contentSettings: {
        contentDisposition: 'attachment' 
    }
};

beforeUpload();

let finishedOrError = false;

// プログレスなどが取得できる、オブジェクトが返却される
const speedSummary = blobService.createBlockBlobFromBrowserFile(CONTAINER_NAME, file.name, file, options, (error, result, response) => {
    finishedOrError = true;
    if (error) {
        console.error('upload error');
        return;
    }
    console.log('upload successfully');
    afterUpload();
});
```
ドキュメントは[こちら](https://azure.github.io/azure-storage-node/BlobService.html#createContainerIfNotExists__anchor)

### 5. プログレス表示

アップロード時に返却される、speedSummaryオブジェクトからプログレス情報を取得可能なため、
一定時間毎にプログレスバーを更新することで実現可能。

```js
// プログレスなどが取得できる、オブジェクトが返却される
const speedSummary = blobService.createBlockBlobFromBrowserFile(CONTAINER_NAME, file.name, file, options, (error, result, response) => {
    finishedOrError = true;
    if (error) {
        console.error('upload error');
        return;
    }
    console.log('upload successfully');
    afterUpload();
});

function refreshProgress() {
    setTimeout(() => {
        if (!finishedOrError) {
            $('#progress').val(speedSummary.getCompletePercent());
            refreshProgress();
        }
    }, 200);
}

refreshProgress();
```

### 6. ダウンロード
コンテナ名、ファイル名、SASトークンからダウンロードURLを生成可能。

```js
const downloadUrl = blobService.getUrl(CONTAINER_NAME, file.name, SAS_TOKEN);
$('#image').attr('src', downloadUrl);
```

ドキュメントは[こちら](https://azure.github.io/azure-storage-node/BlobService.html#getUrl__anchor)

# あとがき

分割アップロードを自分で実装するとなると、そこそこ面倒なため、こういうライブラリがあるのは嬉しい。
ただ、アップロードのキャンセルができない(APIがない)のが、うーむ...という感じ。
用意されていないのは、分割してリクエストを送っている分、うまいこと制御するのが難しかったりするのかも。

# 参考情報
- [Azure Storage JavaScript Client Library for Browsers](https://github.com/Azure/azure-storage-node/blob/master/browser/README.md)
- [New Azure Storage JavaScript client library for browsers - Preview](https://azure.microsoft.com/ja-jp/blog/new-azure-storage-javascript-client-library-for-browsers-preview/)
- [Microsoft Azure Storage SDK for Node.js](https://azure.github.io/azure-storage-node/index.html)