この記事では、JavaのSpringBootを使って、APIからファイルをダウンロードする機能の実装メモを残します。ResponseBodyがないため、@Controller
でも、@RestController
のどちらで定義しても構いません。
クラウドを使用している場合はAWSであれば、S3で保存して認証付きURLを生成すれば十分だと思います。ですので、オンプレミスのDBやファイルサーバ上で管理しているファイルをアプリケーションサーバを通じてダウンロードさせる方法となります。
また、ブラウザ上でアクセスすることを想定しています。cURL
等のアクセスではファイルダウンロードは実行されません。
環境
- Java
- 16
- Spring Boot
- 2.5.5
- Chrome
ゴール
- Spring Bootでファイルをダウンロードさせる
- テストを書く
書かないこと
- ファイルダウンロードする際の認証について
対応
エンドポイントを生成する
今回はRest APIで提供したいので、適当なエンドポイント/downloadFile/{file名}
を用意します。
@GetMapping("/downloadFile/{fileName:.+}") public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) }
Resourceのインスタンスを生成する
Resource
インタフェースのインスタンスを生成します。今回は、オンプレミスサーバにあるファイルのインスタンスを生成したいので、PathResource
を使用します。パスは参照できればどこに配備しても問題ありません。今回はtmp
ディレクトリ直下にファイルがある前提です。
Path path = Path.of("tmp", fileName); Resource resource = new PathResource(path);
ContentType(MIME タイプ)を取得する
返却時のファイルのPath
からContentType
(MIME タイプ)を取得します。
Files.probeContentType(path)
でString
のContentType
を取得できます。MediaType#parseMediaType(string)
でString
型からMediaType
に変換します。
ファイル名によっては変換できないので、その場合はapplication/octet-stream
を返却します。
private MediaType getContentType(Path path) throws IOException { try { return MediaType.parseMediaType(Files.probeContentType(path)); } catch (IOException e) { log.info("Could not determine file type."); return MediaType.APPLICATION_OCTET_STREAM; } }
Responseを設定する
上で取得したコードを返却値のResponseEntity
に設定します。
return ResponseEntity.ok()
.contentType(getContentType(path))
.body(resource);
ResponseのHttpヘッダを設定する
ResponseのHttpヘッダを設定することで、ダウンロード時のファイル名を設定します。
今回は、エンドポイントのパスパラメータを返却用のファイル名にします。
ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
すべての処理をまとめると次のコードになります。
@Validated @Slf4j @Controller @RequestMapping("/files") public class FileController { @GetMapping("/downloadFile/{fileName:.+}") public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) throws Exception { Path path = Path.of("tmp", fileName); Resource resource = new PathResource(path); return ResponseEntity.ok() .contentType(getContentType(path)) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"") .body(resource); } private MediaType getContentType(Path path) throws IOException { try { return MediaType.parseMediaType(Files.probeContentType(path)); } catch (IOException e) { log.info("Could not determine file type."); return MediaType.APPLICATION_OCTET_STREAM; } } }
この状態でアプリケーションをbootRun
して、ブラウザのアドレスバーからhttp://localhost:8080/files/downloadFile/test.pdf
にアクセスすると、ファイルがダウンロードされます。
テストコード
特に解説はしません。このコードで動作検証していました。
なお、HttpのHeaderの設定やContent-Typeの設定は確認できますが、実際にファイルがダウンロードされるかどうかは、ちゃんとブラウザでチェックしましょう。
@AutoConfigureWebClient @WebMvcTest(FileRestController.class) class FileRestControllerTests { @Autowired private MockMvc mockMvc; private final String rootUrl = "/files"; private final Path path = Path.of("tmp/test.pdf"); @BeforeEach void setup() throws IOException { FileUtils.copyToFile( getClass().getResourceAsStream("/kirimaru/test.pdf"), path.toFile() ); } @AfterEach void tearDown() { FileUtils.deleteQuietly( path.toFile() ); } @Test void test_01() throws Exception { String urlPath = "/downloadFile/test.pdf"; MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(rootUrl + urlPath)) .andExpect(status().isOk()) .andExpect(content().contentType("application/pdf")) .andExpect( header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.pdf\"")) .andReturn(); byte[] body = mvcResult.getResponse().getContentAsByteArray(); assertThat(body).isEqualTo(Files.readAllBytes(path)); } } }
ソースコード
- https://github.com/hirotoKirimaru/cucumber-sample/blob/5d27fa9247e62bed917446f928b0f97ebcb80629/kirimaru-backend/src/main/java/kirimaru/restapi/FileRestController.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/5f9cd3696350bcfe8df7fc947c8010ee3976ca4d/kirimaru-backend/src/test/java/kirimaru/restapi/FileRestControllerTests.java
終わりに
テーブルのレイアウトの右端にある「Download」リンクを押すと、ファイルがダウンロードされる、という仕様を想定していました。
冒頭で書いたとおり、ユースケース上は使うことがない機能かもしれませんが、昔を懐かしんで実装しました。この記事で悩んでいる人に対してリーチできたら幸いです。