Azure Storage

Azure FunctionsでAzure Blob Storageのコンテナからファイルをダウンロードしてみた

以前、Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納する処理を実施してみたが、今回はそのサンプルプログラムに、Azure Blob Storageのコンテナからファイルをダウンロードする処理を追加してみたので、共有する。

前提条件

下記記事の実装が完了していること。

Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納してみた前回、Azure Blob Storageのコンテナー内にファイルを格納するプログラムを作成したが、その処理は、App Serviceの...

作成したサンプルプログラム(App Service側)の内容

作成したサンプルプログラム(App Service側)の構成は以下の通り。
サンプルプログラムの構成(AppService)
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

コントローラクラスの内容は以下の通りで、indexメソッドにAzure Blob Storageのコンテナからファイルをダウンロードする処理を、downloadメソッドにダウンロード処理を、それぞれ追加している。なお、Azure Blob Storageのコンテナからファイルをダウンロードする処理については、後述の「作成したサンプルプログラム(Azure Functions側)」にて記載する。

package com.example.demo;

import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.thymeleaf.util.StringUtils;

import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class DemoController {

    /** RestTemplateオブジェクト */
    @Autowired
    private RestTemplate restTemplate;

    /** ObjectMapperオブジェクト */
    @Autowired
    private ObjectMapper objectMapper;

    /** application.propertiesからdemoAzureFunc.urlBaseの値を取得 */
    @Value("${demoAzureFunc.urlBase}")
    private String demoAzureFuncBase;

    /**
     * ファイルリストを取得し、メイン画面を初期表示する.
     * @param model   Modelオブジェクト
     * @param session HttpSessionオブジェクト
     * @return メイン画面
     */
    @GetMapping("/")
    public String index(Model model, HttpSession session) {
        // Azure FunctionsのgetFileList関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのgetFileList関数を呼び出す
        ResponseEntity<String> response = restTemplate.exchange(
              demoAzureFuncBase + "getFileList", HttpMethod.POST,
              new HttpEntity<>(headers), String.class);

        // 取得したファイルリストをModelとセッションに設定する
        GetFileListResult getFileListResult = null;
        try {
            getFileListResult = objectMapper.readValue(
                  response.getBody(), GetFileListResult.class);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        model.addAttribute("fileDataList", getFileListResult.getFileDataList());
        session.setAttribute("fileDataList", getFileListResult.getFileDataList());

        model.addAttribute("message"
           , "アップロードするファイルを指定し、アップロードボタンを押下してください。");
        return "main";
    }

    /**
     * ファイルダウンロード処理.
     * @param id       ID
     * @param response HttpServletResponse
     * @param session  HttpSessionオブジェクト
     * @return 画面遷移先(nullを返す)
     */
    @RequestMapping("/download")
    public String download(@RequestParam("id") String id
           , HttpServletResponse response, HttpSession session) {
        // セッションからファイルリストを取得する
        @SuppressWarnings("unchecked")
        ArrayList<FileData> fileDataList 
            = (ArrayList<FileData>) session.getAttribute("fileDataList");
        FileData fileData = fileDataList.get(Integer.parseInt(id) - 1);

        // ファイルダウンロードの設定を実施
        // ファイルの種類は指定しない
        response.setContentType("application/octet-stream");
        response.setHeader("Cache-Control", "private");
        response.setHeader("Pragma", "");
        response.setHeader("Content-Disposition",
            "attachment;filename=\"" 
            + getFileNameEncoded(fileData.getFileName()) + "\"");

        // ダウンロードファイルへ出力
        try (OutputStream out = response.getOutputStream()) {
            out.write(fileData.getFileData());
            out.flush();
        } catch (Exception e) {
            System.err.println(e);
        }

        // 画面遷移先はnullを指定
        return null;
    }

    /**
     * ファイルデータをAzure Blob Storageに登録する.
     * @param uploadFile         アップロードファイル
     * @param model              Modelオブジェクト
     * @param redirectAttributes リダイレクト先に渡すパラメータ
     * @return メイン画面
     */
    @PostMapping("/upload")
    public String add(@RequestParam("upload_file") MultipartFile uploadFile
        , Model model, RedirectAttributes redirectAttributes) {
        // ファイルが未指定の場合はエラーとする
        if (uploadFile == null 
              || StringUtils.isEmptyOrWhitespace(uploadFile.getOriginalFilename())) {
            redirectAttributes.addFlashAttribute("errMessage"
               , "ファイルを指定してください。");
            return "redirect:/";
        }

        // Azure FunctionsのfileUpload関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのfileUpload関数を呼び出すための引数を設定する
        MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        try {
            map.add("fileName", uploadFile.getOriginalFilename());
            map.add("fileData", IOUtils.toByteArray(uploadFile.getInputStream()));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        HttpEntity<MultiValueMap<String, Object>> entity 
            = new HttpEntity<>(map, headers);

        // Azure FunctionsのfileUpload関数を呼び出す
        ResponseEntity<String> response = restTemplate.exchange(
             demoAzureFuncBase + "fileUpload", HttpMethod.POST,
             entity, String.class);

        // ファイルアップロード処理完了のメッセージを設定する
        FileUploadResult fileUploadResult = null;
        try {
            fileUploadResult = objectMapper.readValue(
                 response.getBody(), FileUploadResult.class);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        // メイン画面へ遷移
        model.addAttribute("message", fileUploadResult.getMessage());
        return "redirect:/";
    }

    /**
     * ファイル名をUTF-8でエンコードする.
     * @param filePath ファイル名
     * @return エンコード後のファイル名
     */
    private String getFileNameEncoded(String fileName) {
        String fileNameAft = "";
        if (!StringUtils.isEmptyOrWhitespace(fileName)) {
            try {
                // ファイル名をUTF-8でエンコードして返却
                fileNameAft = URLEncoder.encode(fileName, "UTF-8");
            } catch (Exception e) {
                System.err.println(e);
                return "";
            }
        }
        return fileNameAft;
    }

}

Azure Blob Storageのコンテナからファイルをダウンロードする処理の戻り値と、ファイルデータの内容は、以下のクラスで定義している。

package com.example.demo;

import java.util.ArrayList;
import lombok.Data;

@Data
public class GetFileListResult {
	
    /** ファイルデータリスト */
    private ArrayList<FileData> fileDataList;
	
}
package com.example.demo;

import lombok.Data;

@Data
public class FileData {

    /** ID */
    private int id;
    
    /** ファイル名 */
    private String fileName;
    
    /** ファイルパス */
    private String filePath;
    
    /** ファイルデータ */
    private byte[] fileData;
    
}

また、画面のHTMLの内容は以下の通りで、Azure Blob Storageのコンテナからダウンロードしたファイルを一覧で表示している。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>メイン画面</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data" th:action="@{/upload}">
        <th:block th:if="!${#strings.isEmpty(message)}">
            <span th:text="${message}">メッセージ</span><br/><br/>
        </th:block>
        <th:block th:if="!${#strings.isEmpty(errMessage)}">
            <font color="#FF0000"><
                span th:text="${errMessage}">エラーメッセージ</span>
            </font><br/><br/>
        </th:block>
        ファイル : <input type="file" name="upload_file" /><br/><br/>
        <input type="submit" value="アップロード" />
    </form>
    <br/><br/>
    アップロードファイルリスト:<br/>
    <table border="1" cellpadding="5">
    <tr>
        <th>ID</th>
        <th>ファイル名</th>
        <th>ファイルパス</th>
        <th>ファイルダウンロード</th>
    </tr>
    <tr th:each="obj : ${fileDataList}">
        <td th:text="${obj.id}"></td>
        <td th:text="${obj.fileName}"></td>
        <td th:text="${obj.filePath}"></td>
        <td>
            <!-- ダウンロードボタンを表示 -->
            <form action="#" method="get"
                  th:action="@{/download(id=${'__${obj.id}__'})}"
                  th:method="download" >
                <input type="hidden" name="_method" value="download" />
                <input type="submit" value="ダウンロード" />
            </form>
        </td>
    </tr>
</table>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-blob-storage-download/demoAzureApp

作成したサンプルプログラム(Azure Functions側)の内容

作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
サンプルプログラムの構成(AzureFunctions側)
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。


その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。


spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。

spring-cloud-function-dependenciesのバージョンを最新(3.1.2)にしてみたこれまでこのブログで取り上げてきたAzure Functionsのサンプルプログラムでは、spring-cloud-function-d...

pom.xmlには、以下のcommons-ioの設定を追加している。

<!-- commons-ioの設定 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

ファイルダウンロードを行うサービスクラスの内容は以下の通りで、Azure Storageのコンテナから取得したファイルデータを設定している。

package com.example.service;

import java.io.InputStream;
import java.util.ArrayList;

import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.example.model.FileData;
import com.example.model.GetFileListParam;
import com.example.model.GetFileListResult;
import com.microsoft.azure.storage.CloudStorageAccount;
import com.microsoft.azure.storage.blob.CloudBlobClient;
import com.microsoft.azure.storage.blob.CloudBlobContainer;
import com.microsoft.azure.storage.blob.CloudBlockBlob;
import com.microsoft.azure.storage.blob.ListBlobItem;

@Service
public class GetFileListService {

    /** Azure Storageのアカウント名 */
    @Value("${azure.storage.accountName}")
    private String storageAccountName;

    /** Azure Storageへのアクセスキー */
    @Value("${azure.storage.accessKey}")
    private String storageAccessKey;

    /** Azure StorageのBlobコンテナー名 */
    @Value("${azure.storage.containerName}")
    private String storageContainerName;

    /**
     * ファイルリスト取得処理を行うサービス.
     * @param getFileListParam ファイル取得用Param
     * @return ファイルリスト取得サービスクラスの呼出結果
     */
    public GetFileListResult getFileList(GetFileListParam getFileListParam) {
        GetFileListResult result = new GetFileListResult();

        // ファイルリスト取得処理
        try {
            // Blobストレージへの接続文字列
            String storageConnectionString = "DefaultEndpointsProtocol=https;" 
                 + "AccountName=" + storageAccountName + ";" 
                 + "AccountKey=" + storageAccessKey + ";";

            // ストレージアカウントオブジェクトを取得
            CloudStorageAccount storageAccount 
                 = CloudStorageAccount.parse(storageConnectionString);

            // Blobクライアントオブジェクトを取得
            CloudBlobClient blobClient = storageAccount.createCloudBlobClient();

            // Blob内のコンテナーを取得
            CloudBlobContainer container 
                  = blobClient.getContainerReference(storageContainerName);

            // Blob情報を取得し、戻り値に設定する
            ArrayList<FileData> fileDataList = new ArrayList<>();
            int index = 1;
            for (ListBlobItem blobItem : container.listBlobs()) {
                // ファイル名・ファイルパスを取得
                String filePath = blobItem.getUri().toString();
                String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);

                // Blobデータを読み込み
                CloudBlockBlob blob = container.getBlockBlobReference(fileName);
                InputStream input = blob.openInputStream();

                // ファイルデータを設定
                FileData fileData = new FileData();
                fileData.setId(index);
                fileData.setFileName(fileName);
                fileData.setFilePath(filePath);
                fileData.setFileData(IOUtils.toByteArray(input));
                fileDataList.add(fileData);
                index++;
            }
            result.setFileDataList(fileDataList);

        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        return result;
    }
}

また、サービスクラスの入出力項目の内容は、以下の通り。「GetFileListResult.java」「FileData.java」の内容は、格納するパッケージ以外はApp Service側と同じ内容になっている。

package com.example.model;

import lombok.Data;

@Data
public class FileData {

    /** ID */
    private int id;
    
    /** ファイル名 */
    private String fileName;
    
    /** ファイルパス */
    private String filePath;
    
    /** ファイルデータ */
    private byte[] fileData;
    
}
package com.example.model;

import lombok.Data;

@Data
public class GetFileListParam {

}
package com.example.model;

import java.util.ArrayList;
import lombok.Data;

@Data
public class GetFileListResult {
	
    /** ファイルデータリスト */
    private ArrayList<FileData> fileDataList;
	
}

さらに、メインクラスとハンドラークラスの内容は、以下の通り。

package com.example;

import java.util.function.Function;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.example.model.FileUploadParam;
import com.example.model.FileUploadResult;
import com.example.model.GetFileListParam;
import com.example.model.GetFileListResult;
import com.example.service.FileUploadService;
import com.example.service.GetFileListService;

@SpringBootApplication
public class DemoAzureFunction {

    /** ファイルアップロードサービスクラスのオブジェクト */
    @Autowired
    private FileUploadService fileUploadService;
    
    /** ファイルリスト取得サービスクラスのオブジェクト */
    @Autowired
    private GetFileListService getFileListService;

    public static void main(String[] args) throws Exception {
        SpringApplication.run(DemoAzureFunction.class, args);
    }

    /**
     * ファイルアップロードを行い結果を返却する関数
     * @return ファイルアップロードサービスクラスの呼出結果
     */
    @Bean
    public Function<FileUploadParam, FileUploadResult> fileUpload() {
        return fileUploadParam 
             -> fileUploadService.fileUpload(fileUploadParam);
    }
    
    /**
     * ファイルリスト取得を行い結果を返却する関数
     * @return ファイルリスト取得サービスクラスの呼出結果
     */
    @Bean
    public Function<GetFileListParam, GetFileListResult> getFileList(){
        return getFileListParam 
             -> getFileListService.getFileList(getFileListParam);
    }
}
package com.example;

import java.util.Optional;

import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler;

import com.example.model.GetFileListParam;
import com.example.model.GetFileListResult;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

public class GetFileListHandler 
    extends AzureSpringBootRequestHandler<GetFileListParam, GetFileListResult> {

    /**
     * HTTP要求に応じて、DemoAzureFunctionクラスのfileUploadメソッドを呼び出し、
     * その戻り値をボディに設定したレスポンスを返す.
     * @param request リクエストオブジェクト
     * @param context コンテキストオブジェクト
     * @return レスポンスオブジェクト
     */
    @FunctionName("getFileList")
    public HttpResponseMessage execute(
        @HttpTrigger(name = "request", methods = HttpMethod.POST
           , authLevel = AuthorizationLevel.ANONYMOUS) 
                HttpRequestMessage<Optional<String>> request
           , ExecutionContext context) {

        // handleRequestメソッド内でDemoAzureFunctionクラスのfileUploadメソッドを
        // 呼び出し、その戻り値をボディに設定したレスポンスを、JSON形式で返す
        return request.createResponseBuilder(HttpStatus.OK)
                .body(handleRequest(new GetFileListParam(), context))
                .header("Content-Type", "text/json").build();
    }
}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/azure-blob-storage-download/demoAzureFunc

作成したサンプルプログラムの実行結果

サンプルプログラムをAzure App Service、Azure Functionsにデプロイして実行した結果は、以下の通り。

1) サンプルプログラム実行前のBlobコンテナーの内容は以下の通りで、3ファイルがアップロードされていることが確認できる。
サンプルプログラムの実行結果_1

2) Azure App ServiceのURL「https://azureappdemoservice.azurewebsites.net/」とアクセスすると、Blobコンテナー内のファイルが「アップロードファイルリスト」に表示されることが確認できる。
サンプルプログラムの実行結果_2_1

なお、Azure App ServiceのURLは、下記Azure App ServiceのURLから確認できる。
サンプルプログラムの実行結果_2_2

3) ファイルアップロードリストの「テスト.pdf」の「ダウンロード」ボタンを押下する。
サンプルプログラムの実行結果_3

4) 画面左下にダウンロードしたファイルが表示され、ダウンロードフォルダ下に「てすと.pdf」が出力されることが確認できる。
サンプルプログラムの実行結果_4_1

サンプルプログラムの実行結果_4_2

また、ダウンロードした「てすと.pdf」の中身は以下の通り。このファイルの中身は、アップロードしたファイルと同じものになっている。
サンプルプログラムの実行結果_4_3

要点まとめ

  • Azure Blob Storageのコンテナからファイルをダウンロードする処理も、Azure FunctionsやAzure App Service内で実装できる。