Class TransactionAwareFileStorage
- All Implemented Interfaces:
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();
-
Nested Class Summary
Nested ClassesNested classes/interfaces inherited from interface FileStorage
FileStorage.FileMetadata, FileStorage.StagingUploadInfo -
Method Summary
Modifier and TypeMethodDescriptionbuilder()voidcompleteStagingUpload(String partition, String tempId, InputStream content, long size) 完成暫存上傳 - 統一上傳流程的第二步void刪除永久區檔案voiddeleteStaged(String partition, String tempId) 刪除暫存檔案boolean檢查永久區檔案是否存在booleanexistsStaged(String partition, String tempId) 檢查暫存檔案是否存在getInputStream(String partition, String fileId) 取得永久區檔案的 InputStreamgetMetadata(String partition, String fileId) 取得永久區檔案的 MetadatagetStagedInputStream(String partition, String tempId) 取得暫存檔案的 InputStreamgetStagedMetadata(String partition, String tempId) 取得暫存檔案的 Metadata取得儲存類型名稱取得檔案的存取 URL開啟永久區檔案[offset, offset + length)區段的 InputStream將暫存檔案持久化到永久區prepareStaging(String partition, String filename, String contentType, long size) 準備暫存上傳 - 統一上傳流程的第一步此儲存能否只讀取物件的一個區段store(String partition, String filename, InputStream content, String contentType, long size) 直接儲存檔案到永久區(跳過暫存)
-
Method Details
-
builder
-
persist
Description copied from interface:FileStorage將暫存檔案持久化到永久區
自動生成唯一的 fileId,預設格式為
yyyy-MM-dd/HH/uuid。 業務層只需儲存回傳的 fileId,不需關心檔案實際儲存位置。- Specified by:
persistin interfaceFileStorage- Parameters:
partition- 儲存分區鍵tempId- 暫存檔案 ID- Returns:
- fileId(唯一且可攜的 ASCII-only 儲存識別碼)
-
store
public String store(String partition, String filename, InputStream content, String contentType, long size) Description copied from interface:FileStorage直接儲存檔案到永久區(跳過暫存)- Specified by:
storein interfaceFileStorage- Parameters:
partition- 儲存分區鍵filename- 原始檔名(用於生成 fileId)content- 檔案內容的 InputStreamcontentType- MIME 類型size- 檔案大小- Returns:
- fileId
-
delete
Description copied from interface:FileStorage刪除永久區檔案- Specified by:
deletein interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 ID
-
prepareStaging
public FileStorage.StagingUploadInfo prepareStaging(String partition, String filename, String contentType, long size) Description copied from interface:FileStorage準備暫存上傳 - 統一上傳流程的第一步
建立暫存 ticket。只有 backend 支援直接上傳(如 S3 Presigned/Azure SAS)時, 回傳值才包含 direct upload URL;application HTTP adapter 的 URL 不屬於 storage capability,由 adapter 依 tempId 自行建立。
- Specified by:
prepareStagingin interfaceFileStorage- Parameters:
partition- 儲存分區鍵filename- 原始檔名contentType- MIME 類型size- 檔案大小- Returns:
- StagingUploadInfo(含 tempId,可能含 backend direct upload URL)
-
completeStagingUpload
Description copied from interface:FileStorage完成暫存上傳 - 統一上傳流程的第二步
接收 PUT 請求的檔案內容,完成暫存區上傳。 此方法由 Controller 在收到 PUT 請求時呼叫。
- Specified by:
completeStagingUploadin interfaceFileStorage- Parameters:
partition- 儲存分區鍵tempId- 暫存檔案 ID(由 prepareStaging 回傳)content- 檔案內容的 InputStreamsize- 檔案大小
-
getStagedMetadata
Description copied from interface:FileStorage取得暫存檔案的 Metadata- Specified by:
getStagedMetadatain interfaceFileStorage- 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:
getStagedInputStreamin interfaceFileStorage- Parameters:
partition- 儲存分區鍵tempId- 暫存檔案 ID- Returns:
- InputStream(呼叫端負責關閉)
- Throws:
FileNotFoundException- 若檔案不存在
-
existsStaged
Description copied from interface:FileStorage檢查暫存檔案是否存在- Specified by:
existsStagedin interfaceFileStorage- Parameters:
partition- 儲存分區鍵tempId- 暫存檔案 ID- Returns:
- 是否存在
-
deleteStaged
Description copied from interface:FileStorage刪除暫存檔案- Specified by:
deleteStagedin interfaceFileStorage- Parameters:
partition- 儲存分區鍵tempId- 暫存檔案 ID
-
getMetadata
Description copied from interface:FileStorage取得永久區檔案的 Metadata- Specified by:
getMetadatain interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 ID- Returns:
- 檔案 Metadata,若不存在則回傳 empty
-
getInputStream
Description copied from interface:FileStorage取得永久區檔案的 InputStream- Specified by:
getInputStreamin interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 ID- Returns:
- InputStream(呼叫端負責關閉)
- Throws:
FileNotFoundException- 若檔案不存在
-
rangeSupport
Description copied from interface:FileStorage此儲存能否只讀取物件的一個區段
預設為
RangeSupport.NONE。只讀取所請求區段(不讀取前段、不讀取完整物件)的實作才可回傳RangeSupport.NATIVE並覆寫FileStorage.openRange(String, String, long, long)。- Specified by:
rangeSupportin interfaceFileStorage- Returns:
- 區段讀取能力
-
openRange
public InputStream openRange(String partition, String fileId, long offset, long length) throws FileNotFoundException Description copied from interface:FileStorage開啟永久區檔案
[offset, offset + length)區段的 InputStream串流恰好產生
min(length, size - offset)個位元組;offset大於等於檔案大小時不產生任何位元組。 fileId 為 write-once,區段讀取不需比對版本。- Specified by:
openRangein interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 IDoffset- 起始位置(0-based,不可為負)length- 最多讀取的位元組數(必須大於 0)- Returns:
- InputStream(呼叫端負責關閉)
- Throws:
FileNotFoundException- 若檔案不存在
-
exists
Description copied from interface:FileStorage檢查永久區檔案是否存在- Specified by:
existsin interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 ID- Returns:
- 是否存在
-
getUrl
Description copied from interface:FileStorage取得檔案的存取 URL
注意:此方法僅適用於雲端儲存(S3、Azure Blob),可回傳 Presigned URL 或 CDN URL。 對於 Local 和 Database 儲存類型,此方法會拋出
UnsupportedOperationException, 因為檔案下載應由業務層 Controller 實作以確保權限控管。- Specified by:
getUrlin interfaceFileStorage- Parameters:
partition- 儲存分區鍵fileId- 檔案 ID- Returns:
- 存取 URL(僅雲端儲存支援)
-
getStorageType
Description copied from interface:FileStorage取得儲存類型名稱- Specified by:
getStorageTypein interfaceFileStorage- Returns:
- 儲存類型(例如 "local", "azure", "s3", "database", "sftp")
-