きり丸の技術日記

技術検証したり、資格等をここに残していきます。

SpringでAPIからファイルをダウンロードする(Java)

この記事では、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)StringContentTypeを取得できます。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));
    }
  }
}

ソースコード

終わりに

テーブルのレイアウトの右端にある「Download」リンクを押すと、ファイルがダウンロードされる、という仕様を想定していました。

冒頭で書いたとおり、ユースケース上は使うことがない機能かもしれませんが、昔を懐かしんで実装しました。この記事で悩んでいる人に対してリーチできたら幸いです。

参考情報

類似情報