LaravelでAzure Blob Storageのデータを取得・更新・削除する

# はじめに
この記事は`Azure Blob Storage`の操作方法について書いています。
`Laravel`のHTTPクライアントと`Azure Storage REST API`を使って説明しています。

https://readouble.com/laravel/10.x/ja/http-client.html

https://learn.microsoft.com/en-ca/rest/api/storageservices/

# 前提
* Laravelの環境構築が済んでいる
* Azure Blob Storageを構築が済んでいる
# 操作方法
取得、更新、削除でそれぞれ細かい処理の違いはありますが、大まかに次の流れになっています。
1. 署名文字列を作成
1. ヘッダーを作成
1. リクエスト

署名文字列はアクセスする際に承認を得るために必要なものです。
詳しく知りたい方は下記を参照してみてください。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/authorize-with-shared-key

## 更新する方法について
`Azure Storage REST API`の`Put Blob`を使います。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/put-blob?tabs=azure-ad

下記のようなコードになります。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Http;

class AzureBlobController extends Controller
{    
    public function save_blob()
    {
        $accesskey = ''; // アクセスキーを入れてください
        $storage_account = ''; // ストレージアカウント名を入れてください
        $container_name = ''; // コンテナ名を入れてください
        $current_date = gmdate('D, d M Y H:i:s T', time());

        $file_to_upload = realpath(''); // ファイルのパスを入れてください(storage/app/public配下のファイル等)
        $blob_name = ''; // Blob名を入れてください
        $destination_url = "https://$storage_account.blob.core.windows.net/$container_name/$blob_name";

        $handle = fopen($file_to_upload, 'r');
        $file_len = filesize($file_to_upload);
        $header_resource = "x-ms-blob-cache-control:max-age=3600\nx-ms-blob-type:BlockBlob\nx-ms-date:$current_date\nx-ms-version:2019-12-12";
        $url_resource = "/$storage_account/$container_name/$blob_name";
        $array_sign = [];
        $array_sign[] = 'PUT'; /*HTTP Verb*/
        $array_sign[] = ''; /*Content-Encoding*/
        $array_sign[] = ''; /*Content-Language*/
        $array_sign[] = $file_len; /*Content-Length (include value when zero)*/
        $array_sign[] = ''; /*Content-MD5*/
        $array_sign[] = 'image/plain'; /*Content-Type*/
        $array_sign[] = ''; /*Date*/
        $array_sign[] = ''; /*If-Modified-Since */
        $array_sign[] = ''; /*If-Match*/
        $array_sign[] = ''; /*If-None-Match*/
        $array_sign[] = ''; /*If-Unmodified-Since*/
        $array_sign[] = ''; /*Range*/
        $array_sign[] = $header_resource; /*CanonicalizedHeaders*/
        $array_sign[] = $url_resource; /*CanonicalizedResource*/
        $str_to_sign = implode("\n", $array_sign);
        $sig = base64_encode(hash_hmac('sha256', urldecode(utf8_encode($str_to_sign)), base64_decode($accesskey), true));
        $auth_header = "SharedKey $storage_account:$sig";

        $headers = [
            'Authorization' => $auth_header,
            'x-ms-blob-cache-control' => 'max-age=3600',
            'x-ms-blob-type' => 'BlockBlob',
            'x-ms-date' => $current_date,
            'x-ms-version' => '2019-12-12',
            'Content-Type' => 'image/plain',
            'Content-Length' => $file_len,
        ];

        $response = Http::withHeaders($headers)->withOptions([
            'verify' => false,
        ])
            ->withBody($handle, 'image/plain')
            ->put($destination_url);

        var_dump($response->body()); // 空文字であれば成功
    }
    }
}
```
実行後に下記のようなエラーメッセージが出力される場合は、署名文字列が正しく作成できているか、ヘッダーの内容が正しいかを確認してみてください。
```xml
<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.
// 略
```

## 取得する方法について
2パターンの取得方法について説明します。1つはBlobを`xml`のリストで取得する`List Blobs`について、もう一つは指定したBlobを1件取得する`Get Blob`についてです。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/list-blobs?tabs=azure-ad

https://learn.microsoft.com/ja-jp/rest/api/storageservices/get-blob?tabs=azure-ad

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Http;

class AzureBlobController extends Controller
{
    public function get_blobs()
    {
        $accesskey = ''; // アクセスキーを入れてください
        $storage_account = ''; // ストレージアカウント名を入れてください
        $container_name = ''; // コンテナ名を入れてください
        $current_date = gmdate('D, d M Y H:i:s T', time());

        $destination_url = "https://$storage_account.blob.core.windows.net/$container_name?restype=container&comp=list";

        $header_resource = "x-ms-date:$current_date\nx-ms-version:2023-08-03";
        $url_resource = "/$storage_account/$container_name\ncomp:list\nrestype:container";
        $array_sign = [];
        $array_sign[] = 'GET'; /*HTTP Verb*/
        $array_sign[] = ''; /*Content-Encoding*/
        $array_sign[] = ''; /*Content-Language*/
        $array_sign[] = ''; /*Cont ent-Length (include value when zero)*/
        $array_sign[] = ''; /*Content-MD5*/
        $array_sign[] = ''; /*Content-Type*/
        $array_sign[] = ''; /*Date*/
        $array_sign[] = ''; /*If-Modified-Since */
        $array_sign[] = ''; /*If-Match*/
        $array_sign[] = ''; /*If-None-Match*/
        $array_sign[] = ''; /*If-Unmodified-Since*/
        $array_sign[] = ''; /*Range*/
        $array_sign[] = $header_resource; /*CanonicalizedHeaders*/
        $array_sign[] = $url_resource; /*CanonicalizedResource*/
        $str_to_sign = implode("\n", $array_sign);
        $sig = base64_encode(hash_hmac('sha256', urldecode(utf8_encode($str_to_sign)), base64_decode($accesskey), true));
        $auth_header = "SharedKey $storage_account:$sig";

        $headers = [
            'Authorization' => $auth_header,
            'x-ms-date' => $current_date,
            'x-ms-version' => '2023-08-03',
        ];
        $response = Http::withHeaders($headers)->get($destination_url);

        // xml文字列でレスポンスが返ってくるためオブジェクトに変換
        $response_blobs = [];
        $xml = simplexml_load_string($response->body());
        // xmlのリストからBlob名を取り出して、1件ずつBlobを取得している
        foreach ($xml->Blobs->Blob as $blob) {
            $encoded_name = (string) $blob->Name;
            array_push($response_blobs, $this->get_blob($encoded_name));
        }
        echo '<pre>';
        var_dump($response_blobs); // ファイルの中身が配列に格納されていれば成功
        echo '</pre>';
    }

    public function get_blob($blob_name)
    {
        $accesskey = ''; // アクセスキーを入れてください
        $storage_account = ''; // ストレージアカウント名を入れてください
        $container_name = ''; // コンテナ名を入れてください
        $current_date = gmdate('D, d M Y H:i:s T', time());

        $destination_url = "https://$storage_account.blob.core.windows.net/$container_name/$blob_name";
        $blob_name_encoded = urlencode($blob_name);
        $header_resource = "x-ms-date:$current_date\nx-ms-version:2023-08-03";
        $url_resource = "/$storage_account/$container_name/$blob_name_encoded";

        $array_sign = [];
        $array_sign[] = 'GET'; /*HTTP Verb*/
        $array_sign[] = ''; /*Content-Encoding*/
        $array_sign[] = ''; /*Content-Language*/
        $array_sign[] = ''; /*Cont ent-Length (include value when zero)*/
        $array_sign[] = ''; /*Content-MD5*/
        $array_sign[] = ''; /*Content-Type*/
        $array_sign[] = ''; /*Date*/
        $array_sign[] = ''; /*If-Modified-Since */
        $array_sign[] = ''; /*If-Match*/
        $array_sign[] = ''; /*If-None-Match*/
        $array_sign[] = ''; /*If-Unmodified-Since*/
        $array_sign[] = ''; /*Range*/
        $array_sign[] = $header_resource; /*CanonicalizedHeaders*/
        $array_sign[] = $url_resource; /*CanonicalizedResource*/
        $str_to_sign = implode("\n", $array_sign);
        $sig = base64_encode(hash_hmac('sha256', urldecode(utf8_encode($str_to_sign)), base64_decode($accesskey), true));
        $auth_header = "SharedKey $storage_account:$sig";

        $headers = [
            'Authorization' => $auth_header,
            'x-ms-date' => $current_date,
            'x-ms-version' => '2023-08-03',
        ];
        $response = Http::withHeaders($headers)->get($destination_url);

        return $response->body();
    }
}
```

## 削除する方法について
最後に削除する方法について、こちらは`Delete Blob`を使用します。

https://learn.microsoft.com/ja-jp/rest/api/storageservices/delete-blob?tabs=azure-ad

下記のようなコードになります。
```php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Http;

class AzureBlobController extends Controller
{
    public function delete_blob()
    {
        $blob_name = ''; // 削除したいBlob名を入れてください
        $accesskey = ''; // アクセスキーを入れてください
        $storage_account = ''; // ストレージアカウント名を入れてください
        $container_name = ''; // コンテナ名を入れてください
        $current_date = gmdate('D, d M Y H:i:s T', time());

        $destination_url = "https://$storage_account.blob.core.windows.net/$container_name/$blob_name";
        $blob_name_encoded = urlencode($blob_name);
        $header_resource = "x-ms-date:$current_date\nx-ms-version:2023-08-03";
        $url_resource = "/$storage_account/$container_name/$blob_name_encoded";

        $array_sign = [];
        $array_sign[] = 'DELETE'; /*HTTP Verb*/
        $array_sign[] = ''; /*Content-Encoding*/
        $array_sign[] = ''; /*Content-Language*/
        $array_sign[] = ''; /*Cont ent-Length (include value when zero)*/
        $array_sign[] = ''; /*Content-MD5*/
        $array_sign[] = ''; /*Content-Type*/
        $array_sign[] = ''; /*Date*/
        $array_sign[] = ''; /*If-Modified-Since */
        $array_sign[] = ''; /*If-Match*/
        $array_sign[] = ''; /*If-None-Match*/
        $array_sign[] = ''; /*If-Unmodified-Since*/
        $array_sign[] = ''; /*Range*/
        $array_sign[] = $header_resource; /*CanonicalizedHeaders*/
        $array_sign[] = $url_resource; /*CanonicalizedResource*/
        $str_to_sign = implode("\n", $array_sign);
        $sig = base64_encode(hash_hmac('sha256', urldecode(utf8_encode($str_to_sign)), base64_decode($accesskey), true));
        $auth_header = "SharedKey $storage_account:$sig";

        $headers = [
            'Authorization' => $auth_header,
            'x-ms-date' => $current_date,
            'x-ms-version' => '2023-08-03',
        ];

        $response = Http::withHeaders($headers)->delete($destination_url);
        var_dump($response->body()); // 空文字であれば成功
    }
}
```

# 終わりに
ここまで見ていただきありがとうございました。
`Azure Blob Storage`への接続処理を実装した際に、署名文字列の作成がうまくいかず苦戦しました。解決する過程で色々な記事を見ましたが、同じように苦戦している方が多かったので記事にしようと思いました。

内容がほぼ同じにはなりますが、`PHP`の`curl_setopt`でもアクセス方法もそのうち書こうと思っています。

# 参考
https://qiita.com/yz2cm/items/71067ccea24a948ad772

https://readouble.com/laravel/10.x/ja/http-client.html

https://learn.microsoft.com/en-ca/rest/api/storageservices/

https://learn.microsoft.com/ja-jp/rest/api/storageservices/authorize-with-shared-key

https://learn.microsoft.com/ja-jp/rest/api/storageservices/get-blob?tabs=azure-ad

https://learn.microsoft.com/ja-jp/rest/api/storageservices/get-block-list?tabs=azure-ad

https://learn.microsoft.com/ja-jp/rest/api/storageservices/put-blob?tabs=azure-ad

https://learn.microsoft.com/ja-jp/rest/api/storageservices/delete-blob?tabs=azure-ad

https://techcommunity.microsoft.com/t5/apps-on-azure-blog/app-service-to-storage-account-connection-condition-summary/ba-p/2968642

https://azure.microsoft.com/ja-jp/updates/retirement-notice-the-azure-storage-php-client-libraries-will-be-retired-on-17-march-2024/