Node.js で Azure BLOB に SAS トークン を使って、ファイルをアップロードしてダウンロードする

## tl;dr

SAS トークンの構造を学習するために、MSのライブラリを使わないで生成しました。

公式ドキュメント : [サービス SAS を作成する](https://docs.microsoft.com/ja-jp/rest/api/storageservices/create-service-sas
)

## コード
ファイルをアップロードして、すぐさま、ダウンロードします。

[以前のコード](https://qiita.com/exabugs/items/59efb4dacf193bfb4f5d) との違いは、'permission' に 'c' を足したこと。
それ以外は同じで大丈夫みたい。

```javascript:main.js
const axios = require("axios");
const SAS = require("./SAS");
const fs = require("fs");

const name = "ストレージアカウント名";
const key = "キー";
const container = "コンテナ名";
const blob = "ブロブ名";
const filename = "ファイル名";

const expiry = 1; // 1 秒間有効

const sas = new SAS(name, key);

const get = async (blob, filename) => {
    // URL 生成
    const url = sas.getUrl(container, blob, expiry, filename);
    console.log(url);

    await axios({
        method: "get",
        url,
        responseType: "arraybuffer", // axios
    }).then((result) => {
        console.log(result.statusText);
        const name = result.headers["content-disposition"].split("=")[1].slice(1, -1);
        fs.writeFileSync(name, result.data);
    });
};

const put = async (blob, filename) => {
    // URL 生成
    const url = sas.getUrl(container, blob, expiry, filename);
    console.log(url);

    const data = fs.readFileSync(filename);
    await axios({
        method: "put",
        url,
        data,
        headers: {
            "Content-Type": "application/octet-stream",
            "x-ms-blob-type": "BlockBlob",
        },
    }).catch((e) => {
        console.log(e);
    });
};

const main = async () => {
    await put(blob, filename);
    await get(blob, filename + ".1");
};

main().then((result) => {
    console.log(result);
});

```

```javascript:SAS.js
const crypto = require("crypto");

class SAS {
    // コンストラクタ
    // @param name ストレージアカウント名
    // @param key ストレージアカウントキー
    constructor(name, key) {
        this.account_name = name;
        this.account_key = key;
    }

    // トークン付きURLを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間
    // @param filename ファイル名
    getUrl(container, blob, expiry_second, filename) {
        const token = this.getToken(container, blob, expiry_second, filename);
        const url = `https://${this.account_name}.blob.core.windows.net/${container}/${blob}`;
        return `${url}?${token}`;
    }

    // トークンを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間
    // @param filename ファイル名
    getToken(container, blob, expiry_second, filename) {
        // パラメータ
        const now = new Date();
        const start = ""; // this.isoDate(now, start_second);
        const expiry = this.isoDate(now, expiry_second);

        const version = "2018-11-09";
        const resource = "b";
        const permission = "rc";
        const ip = ""; // "0.0.0.0/0";
        const protocol = ""; // "https";

        const canonicalizedResource = `/blob/${this.account_name}/${container}/${blob}`;

        const rscc = "";
        const rscd = `attachment; filename="${filename}"`;
        const rsce = "";
        const rscl = "";
        const rsct = "";

        const identifier = "";
        const snapshot = "";

        // シグネチャ (順番は厳守)
        const stringToSign = [
            permission,
            start,
            expiry,
            canonicalizedResource,
            identifier,
            ip,
            protocol,
            version,
            resource,
            snapshot,
            rscc, // Cache-Control
            rscd, // Content-Disposition
            rsce, // Content-Encoding
            rscl, // Content-Language
            rsct, // Content-Type
        ].join("\n");

        // HMAC256
        const signature = this.getHMAC256(stringToSign);

        // クエリパラメータ (順番は任意)
        const sas = [
            ["sp", permission], //
            ["sr", resource],
            // ["st", start],
            ["se", expiry],
            // ["sip", ip],
            // ["spr", protocol],
            ["sv", version],
            ["sig", signature],
            ["rscd", rscd],
        ]
            .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
            .join("&");

        return sas;
    }

    isoDate(now, second = 0) {
        const date = new Date(now.getTime() + second * 1000);
        return date.toISOString().substring(0, 19) + "Z";
    }

    getHMAC256(input) {
        // 1. キー は Buffer (Base64 で デコード)
        const key = Buffer.from(this.account_key, "base64");
        const hmac = crypto.createHmac("sha256", key);
        // 2. 入力 は Buffer (UTF8 で デコード)
        const inp = Buffer.from(input, "utf8");
        // 3. 出力 は Buffer を Base64 で エンコード
        const out = hmac.update(inp).digest();
        return out.toString("base64");
    }

    // 上を簡単に書き直すと
    getHMAC256_(input) {
        const key = Buffer.from(this.account_key, "base64");
        return crypto.createHmac("sha256", key).update(input).digest("base64");
    }
}

module.exports = SAS;
```