Class TransactionAwareFileStorage

java.lang.Object
io.leandev.appfuse.file.tx.TransactionAwareFileStorage
All Implemented Interfaces:
FileStorage

public class TransactionAwareFileStorage extends Object implements FileStorage

交易感知的 FileStorage 裝飾器

把「檔案系統副作用」對齊資料庫交易邊界,消除兩類非交易後端(Local / S3 / Azure / SFTP)特有的不一致:

操作 原問題 對策 Hook
persist / store commit 前必須先寫實體(要拿 fileId 寫 DB)→ rollback 留孤兒 立即執行,rollback 時補償刪除 afterCompletion(STATUS_ROLLED_BACK)
delete 立即刪 → rollback 留懸空參照(DB 還指著已刪檔) 延到 commit 後才真的刪 afterCommit

設計上刻意把所有失敗模式都偏向「孤兒」、永不「懸空」:孤兒只佔空間、使用者看不到,且可由 sweep(下一階段)依 FileJournal 對帳清除;懸空會讓使用者看到壞掉的連結。

不要包 Database 後端

DatabaseFileStorage 的 persist/delete 是交易內的 JPA 操作、隨 rollback 復原,本身已對齊交易。 若用本裝飾器包它,delete 反而會被延到 afterCommit、脫離原交易,弄巧成拙。只包非交易後端。

無交易時的行為

無 active 交易(無同步)時退化為「立即執行、不補償」(等同未包裝):persist/store 直接記為 CONFIRMED、delete 立即執行——呼叫端自負失敗後果。正常路徑下業務 Service 皆為 @Transactional

使用方式

FileStorage storage = TransactionAwareFileStorage.builder()
    .delegate(localFileStorage)
    .journal(localFileJournal)   // 省略則用 NoOpFileJournal(只補償、不寫稽核軌跡)
    .build();
  • Method Details

    • builder

      public static TransactionAwareFileStorage.Builder builder()
    • persist

      public String persist(String partition, String tempId)
      Description copied from interface: FileStorage

      將暫存檔案持久化到永久區

      自動生成唯一的 fileId,格式為 yyyy-MM-dd/HH/filename-shortUuid.ext。 業務層只需儲存回傳的 fileId,不需關心檔案實際儲存位置。

      Specified by:
      persist in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID
      Returns:
      fileId(唯一識別碼,同時也是可讀的路徑格式)
    • store

      public String store(String partition, String filename, InputStream content, String contentType, long size)
      Description copied from interface: FileStorage
      直接儲存檔案到永久區(跳過暫存)
      Specified by:
      store in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      filename - 原始檔名(用於生成 fileId)
      content - 檔案內容的 InputStream
      contentType - MIME 類型
      size - 檔案大小
      Returns:
      fileId
    • delete

      public void delete(String partition, String fileId)
      Description copied from interface: FileStorage
      刪除永久區檔案
      Specified by:
      delete in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      fileId - 檔案 ID
    • prepareStaging

      public FileStorage.StagingUploadInfo prepareStaging(String partition, String filename, String contentType, long size)
      Description copied from interface: FileStorage

      準備暫存上傳 - 統一上傳流程的第一步

      前端先呼叫此方法取得上傳 URL,再透過 PUT 上傳檔案內容。 此設計讓所有儲存類型(Local、Database、S3)使用統一的前端上傳邏輯。

      Specified by:
      prepareStaging in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      filename - 原始檔名
      contentType - MIME 類型
      size - 檔案大小
      Returns:
      StagingUploadInfo(含 tempId 和上傳 URL)
    • completeStagingUpload

      public void completeStagingUpload(String partition, String tempId, InputStream content, long size)
      Description copied from interface: FileStorage

      完成暫存上傳 - 統一上傳流程的第二步

      接收 PUT 請求的檔案內容,完成暫存區上傳。 此方法由 Controller 在收到 PUT 請求時呼叫。

      Specified by:
      completeStagingUpload in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID(由 prepareStaging 回傳)
      content - 檔案內容的 InputStream
      size - 檔案大小
    • getStagedMetadata

      public Optional<FileStorage.FileMetadata> getStagedMetadata(String partition, String tempId)
      Description copied from interface: FileStorage
      取得暫存檔案的 Metadata
      Specified by:
      getStagedMetadata in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID
      Returns:
      檔案 Metadata,若不存在則回傳 empty
    • getStagedInputStream

      public InputStream getStagedInputStream(String partition, String tempId) throws FileNotFoundException
      Description copied from interface: FileStorage
      取得暫存檔案的 InputStream
      Specified by:
      getStagedInputStream in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID
      Returns:
      InputStream(呼叫端負責關閉)
      Throws:
      FileNotFoundException - 若檔案不存在
    • existsStaged

      public boolean existsStaged(String partition, String tempId)
      Description copied from interface: FileStorage
      檢查暫存檔案是否存在
      Specified by:
      existsStaged in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID
      Returns:
      是否存在
    • deleteStaged

      public void deleteStaged(String partition, String tempId)
      Description copied from interface: FileStorage
      刪除暫存檔案
      Specified by:
      deleteStaged in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      tempId - 暫存檔案 ID
    • getMetadata

      public Optional<FileStorage.FileMetadata> getMetadata(String partition, String fileId)
      Description copied from interface: FileStorage
      取得永久區檔案的 Metadata
      Specified by:
      getMetadata in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      fileId - 檔案 ID
      Returns:
      檔案 Metadata,若不存在則回傳 empty
    • getInputStream

      public InputStream getInputStream(String partition, String fileId) throws FileNotFoundException
      Description copied from interface: FileStorage
      取得永久區檔案的 InputStream
      Specified by:
      getInputStream in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      fileId - 檔案 ID
      Returns:
      InputStream(呼叫端負責關閉)
      Throws:
      FileNotFoundException - 若檔案不存在
    • exists

      public boolean exists(String partition, String fileId)
      Description copied from interface: FileStorage
      檢查永久區檔案是否存在
      Specified by:
      exists in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      fileId - 檔案 ID
      Returns:
      是否存在
    • getUrl

      public String getUrl(String partition, String fileId)
      Description copied from interface: FileStorage

      取得檔案的存取 URL

      注意:此方法僅適用於雲端儲存(S3、Azure Blob),可回傳 Presigned URL 或 CDN URL。 對於 Local 和 Database 儲存類型,此方法會拋出 UnsupportedOperationException, 因為檔案下載應由業務層 Controller 實作以確保權限控管。

      Specified by:
      getUrl in interface FileStorage
      Parameters:
      partition - 儲存分區鍵
      fileId - 檔案 ID
      Returns:
      存取 URL(僅雲端儲存支援)
    • getStorageType

      public String getStorageType()
      Description copied from interface: FileStorage
      取得儲存類型名稱
      Specified by:
      getStorageType in interface FileStorage
      Returns:
      儲存類型(例如 "local", "azure", "s3", "database", "sftp")