Class FileResponseBuilder

java.lang.Object
io.leandev.appfuse.file.FileResponseBuilder

public class FileResponseBuilder extends Object

檔案下載 Response 建構器

提供統一的檔案下載回應建構,支援:

  • 完整檔案下載(200 OK)
  • 部分內容下載(206 Partial Content)- 支援 Range Request
  • 影音串流(自動處理 Range Request)
  • 斷點續傳

使用範例

@GetMapping("/products/{id}/image")
public ResponseEntity<Resource> getProductImage(@PathVariable String id, HttpServletRequest request) {

    Product product = productService.findById(id);  // 權限檢查
    String partition = TenantContext.getCurrentTenantId();

    // 讀取方法、Range、If-Range、If-None-Match;HEAD 不讀取內容
    return FileResponseBuilder.from(fileStorage, partition, product.getImageFileId())
            .request(request)
            .build();
}

影音串流範例

@GetMapping("/videos/{id}")
public ResponseEntity<Resource> streamVideo(@PathVariable String id, HttpServletRequest request) {

    Video video = videoService.findById(id);
    String partition = TenantContext.getCurrentTenantId();

    // 儲存支援區段讀取(RangeSupport.NATIVE)時,拖動進度條只傳輸所需區段
    return FileResponseBuilder.from(fileStorage, partition, video.getFileId())
            .request(request)
            .build();
}

Range 語意(RFC 9110)

只服務 GET 與 HEAD;其他方法在任何評估之前丟出 IllegalArgumentException。評估順序: 404 → If-None-Match 相符回 304 → HEAD 以 metadata 回 headers、不讀內容 → 沒有 Range、 儲存為 RangeSupport.NONE 或 If-Range 不符時回 200 完整內容 → RangeUtils.evaluate(String, long): 格式錯誤或多個 range 回 200、無法滿足回 416、可滿足回 206(以 FileStorage.openRange(String, String, long, long) 只讀該區段)。 If-Range 只接受與 ETag 相同的強 ETag。跨來源前端(如 PDF.js)需 expose RANGE_EXPOSED_HEADERS。

See Also:
  • Field Details

    • RANGE_EXPOSED_HEADERS

      public static final List<String> RANGE_EXPOSED_HEADERS
      range 回應需要讓跨來源前端讀取的 headers;應用的 CORS 設定把它加入 exposedHeaders
  • Method Details

    • from

      public static FileResponseBuilder from(FileStorage fileStorage, String partition, String fileId)
      建立 FileResponseBuilder
      Parameters:
      fileStorage - FileStorage 實例
      partition - 儲存分區鍵
      fileId - 檔案 ID
      Returns:
      FileResponseBuilder 實例
    • request

      public FileResponseBuilder request(jakarta.servlet.http.HttpServletRequest request)

      由目前請求讀取方法、Range、If-Range 與 If-None-Match

      建議的用法:controller 不必逐一傳入條件式 headers,HEAD 也不會讀取內容。

      Parameters:
      request - 目前請求
      Returns:
      this
    • method

      public FileResponseBuilder method(org.springframework.http.HttpMethod method)

      設定請求方法;未設定時視為 GET

      只接受 GET 與 HEAD,其他方法在 build() 丟出 IllegalArgumentException。 HEAD 以 metadata 建構 headers,不開啟內容串流。

      Parameters:
      method - 請求方法
      Returns:
      this
    • ifRange

      public FileResponseBuilder ifRange(String ifRange)

      設定請求的 If-Range header

      只有與 ETag 完全相同的強 ETag 才處理 Range;弱 ETag、日期形式與空值一律回完整內容。

      Parameters:
      ifRange - 請求的 If-Range header 值;null 表示請求沒有 If-Range
      Returns:
      this
    • range

      public FileResponseBuilder range(String rangeHeader)
      設定 Range header(支援部分內容下載)
      Parameters:
      rangeHeader - HTTP Range header 值(如 "bytes=0-1000"),可為 null
      Returns:
      this
    • filename

      public FileResponseBuilder filename(String filename)

      設定下載時的檔名

      若不設定,會使用檔案的原始檔名。

      Parameters:
      filename - 下載時顯示的檔名
      Returns:
      this
    • forceDownload

      public FileResponseBuilder forceDownload()

      強制下載(而非在瀏覽器中開啟)

      設定 Content-Disposition: attachment,讓瀏覽器下載檔案而非直接開啟。

      Returns:
      this
    • ifNoneMatch

      public FileResponseBuilder ifNoneMatch(String ifNoneMatch)

      設定請求的 If-None-Match header(啟用條件式下載)

      傳入後,build() 會以 ETag(見 build() 的 ETag 說明)比對; 相符則在開啟儲存串流之前直接回 304 Not Modified、不送 body, 同時省下傳輸與 storage 讀取。

      Controller 端以 @RequestHeader(value = "If-None-Match", required = false) 取得。

      Parameters:
      ifNoneMatch - 請求的 If-None-Match header 值,可為 null
      Returns:
      this
    • immutable

      public FileResponseBuilder immutable()

      宣告內容不可變,套用長效快取

      設定 Cache-Control: public, max-age=31536000, immutable,讓瀏覽器與共享快取 快取一年且不再回源驗證。適用於以 fileId 定址的 URL(如 /files/{fileId})—— 因為 fileId 為 write-once(見 FileStorage),同一 fileId 的內容永不改變。

      勿用於「URL 穩定但內容可換」的實體型 URL(如 /products/{id}/image); 那類請維持預設(no-cache + ETag 每次 revalidate),換圖時才不會拿到舊快取。

      Returns:
      this
    • cacheControl

      public FileResponseBuilder cacheControl(org.springframework.http.CacheControl cacheControl)

      覆寫 Cache-Control 策略

      預設為 CacheControl.noCache(帶 ETag、每次 revalidate、命中回 304)。 需要私有快取、短時效或其他策略時以此覆寫,例如 cacheControl(CacheControl.noCache().cachePrivate())。

      Parameters:
      cacheControl - Cache-Control 策略
      Returns:
      this
    • build

      public org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> build()

      建構 ResponseEntity

      • 方法不是 GET/HEAD → IllegalArgumentException(不讀取任何資料)
      • 檔案不存在 → 404 Not Found
      • If-None-Match 與 ETag 相符 → 304 Not Modified(不開串流、不送 body)
      • HEAD → 200,headers 由 metadata 建構,不開串流、不處理 Range
      • 無 Range、儲存不支援區段讀取、If-Range 不符、Range 格式錯誤或多個 range → 200 OK(完整檔案)
      • Range 無法滿足 → 416,Content-Range: bytes */{size}
      • Range 可滿足 → 206 Partial Content(FileStorage.openRange(String, String, long, long) 只讀該區段)

      ETag:以 fileId 為強驗證器("{fileId}")。fileId 為 write-once (見 FileStorage),同一 fileId 的內容永不改變,故可安全作為 ETag。回應一律帶 ETag 與 Cache-Control(預設 no-cache,或 immutable() 的長效策略)。

      Returns:
      ResponseEntity
      Throws:
      IllegalArgumentException - 請求方法不是 GET 或 HEAD