以前、Azure FunctionsでAzure Blob Storageのコンテナー内にファイルを格納する処理を実施してみたが、今回はそのサンプルプログラムに、Azure Blob Storageのコンテナからファイルをダウンロードする処理を追加してみたので、共有する。
前提条件
下記記事の実装が完了していること。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
コントローラクラスの内容は以下の通りで、indexメソッドにAzure Blob Storageのコンテナからファイルをダウンロードする処理を、downloadメソッドにダウンロード処理を、それぞれ追加している。なお、Azure Blob Storageのコンテナからファイルをダウンロードする処理については、後述の「作成したサンプルプログラム(Azure Functions側)」にて記載する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | 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のコンテナからファイルをダウンロードする処理の戻り値と、ファイルデータの内容は、以下のクラスで定義している。
1 2 3 4 5 6 7 8 9 10 11 12 | package com.example.demo; import java.util.ArrayList; import lombok.Data; @Data public class GetFileListResult { /** ファイルデータリスト */ private ArrayList<FileData> fileDataList; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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のコンテナからダウンロードしたファイルを一覧で表示している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <!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側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
<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を利用した実装サンプルは、以下の記事を参照のこと。
pom.xmlには、以下のcommons-ioの設定を追加している。
1 2 3 4 5 6 | <!-- commons-ioの設定 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> |
ファイルダウンロードを行うサービスクラスの内容は以下の通りで、Azure Storageのコンテナから取得したファイルデータを設定している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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側と同じ内容になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.example.model; import lombok.Data; @Data public class FileData { /** ID */ private int id; /** ファイル名 */ private String fileName; /** ファイルパス */ private String filePath; /** ファイルデータ */ private byte[] fileData; } |
1 2 3 4 5 6 7 8 | package com.example.model; import lombok.Data; @Data public class GetFileListParam { } |
1 2 3 4 5 6 7 8 9 10 11 12 | package com.example.model; import java.util.ArrayList; import lombok.Data; @Data public class GetFileListResult { /** ファイルデータリスト */ private ArrayList<FileData> fileDataList; } |
さらに、メインクラスとハンドラークラスの内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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ファイルがアップロードされていることが確認できる。
2) Azure App ServiceのURL「https://azureappdemoservice.azurewebsites.net/」とアクセスすると、Blobコンテナー内のファイルが「アップロードファイルリスト」に表示されることが確認できる。
なお、Azure App ServiceのURLは、下記Azure App ServiceのURLから確認できる。
3) ファイルアップロードリストの「テスト.pdf」の「ダウンロード」ボタンを押下する。
4) 画面左下にダウンロードしたファイルが表示され、ダウンロードフォルダ下に「てすと.pdf」が出力されることが確認できる。
また、ダウンロードした「てすと.pdf」の中身は以下の通り。このファイルの中身は、アップロードしたファイルと同じものになっている。
要点まとめ
- Azure Blob Storageのコンテナからファイルをダウンロードする処理も、Azure FunctionsやAzure App Service内で実装できる。