mirror of
https://github.com/keven1024/015.git
synced 2026-06-07 21:04:33 +00:00
Compare commits
5 Commits
main
...
0.12.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64f3d2e1d5 | ||
|
|
9d125ba9bd | ||
|
|
2e2698e281 | ||
|
|
ab0587bd4d | ||
|
|
ed9d39301f |
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-05-25
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
worker 目前所有檔案清理都依賴 `file:remove` 延遲任務,由業務邏輯在適當時機主動排程(上傳完成後設定 TTL、share 刪除時觸發)。若任務在排程前 worker 重啟、Redis 任務丟失或業務邏輯有 bug,檔案就會永久殘留在磁碟上。`asynq` 已提供 `Scheduler` 元件支援 cron 排程,無需引入新依賴。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 每天凌晨自動掃描並清理三類漏網檔案
|
||||||
|
- 不重複造輪子,清理邏輯複用現有 `file:remove` 任務
|
||||||
|
- 不影響現有任務流程與 API
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不處理 `Expire == 0` 的特殊情況
|
||||||
|
- 不清理 `fileShareRelational` 裡的孤兒 share 條目
|
||||||
|
- 不提供手動觸發的 HTTP 介面
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
**1. 使用 `asynq.Scheduler` 而非外部 cron,排程時間為每天凌晨 03:00**
|
||||||
|
|
||||||
|
`asynq.Scheduler` 在同一 worker 程式內以獨立 goroutine 運行,透過 Redis 做 leader election(避免多實例重複觸發)。外部 cron 需要額外基礎設施。選擇 Scheduler,cron 表達式為 `0 3 * * *`。
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go
|
||||||
|
├── asynq.NewServer(...) // 處理任務
|
||||||
|
├── asynq.NewScheduler(...) // 排程 file:janitor @ 03:00
|
||||||
|
└── mux.HandleFunc("file:janitor", tasks.FileJanitor)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. `FileJanitor` 函式放在 `worker/internal/tasks/file.go`,超過 300 行才拆資料夾**
|
||||||
|
|
||||||
|
目前 `file.go` 為 50 行,加入 janitor 後預計約 100 行,遠低於 300 行門檻,直接寫入 `file.go`。若未來該檔超過 300 行,則改為 `worker/internal/tasks/file/` 目錄,拆成 `janitor.go`(清理邏輯)與 `remove.go`(原 `RemoveFile` 邏輯)。
|
||||||
|
|
||||||
|
**3. Case 1(孤兒本地檔)直接 `os.RemoveAll`,不走 `file:remove`**
|
||||||
|
|
||||||
|
這類檔案在 fileInfoMap 中不存在,`RemoveFile` handler 會直接 return nil(找不到 fileInfo),走 `file:remove` 無法刪除磁碟檔案。必須直接刪除。
|
||||||
|
|
||||||
|
**4. Case 2、3 透過 `SetFileRemoveTask(id, 0)` 委派**
|
||||||
|
|
||||||
|
複用 `RemoveFile` 現有邏輯(含 share 二次確認),避免重複實作。delay=0 表示立即處理。
|
||||||
|
|
||||||
|
**5. 掃描策略:一次性全量讀取**
|
||||||
|
|
||||||
|
- 本地目錄:`os.ReadDir(uploadPath)` 取得所有檔名(即 fileId)
|
||||||
|
- Redis:`GetRedisFileInfoAll()` 一次取得完整 fileInfoMap
|
||||||
|
- 兩者建立 map 後交叉比對,時間複雜度 O(n)
|
||||||
|
|
||||||
|
```
|
||||||
|
本地檔案 set ────┐
|
||||||
|
├─ 差集 → Case 1
|
||||||
|
Redis fileInfoMap ┘
|
||||||
|
|
||||||
|
Redis fileInfoMap ─ 遍歷 ─┬─ type==init && expired → Case 2
|
||||||
|
└─ type==already && no share → Case 3
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. Case 3 的 share 關係查詢**
|
||||||
|
|
||||||
|
對每個 `type==already` 的 fileId 呼叫 `GetRedisFileShareRelational(fileId)`,若回傳空 slice 則觸發刪除。此為 O(n) Redis 查詢,數量級與檔案數相同,凌晨低峰期執行可接受。
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **誤刪風險(Case 1)**:本地檔案剛建立但 fileInfoMap 尚未寫入的時間窗口(上傳初始化中)。→ 凌晨執行,正在上傳中的檔案其 init 記錄通常已存在 Redis,風險極低。
|
||||||
|
- **Redis 查詢量(Case 3)**:若檔案數量龐大,逐一查詢 fileShareRelational 會產生大量 Redis 請求。→ 目前是輕量平台,可接受;未來可改用 `HGETALL fileShareRelational` 一次取得再做本地比對。
|
||||||
|
- **Scheduler 單點**:`asynq.Scheduler` 依賴 Redis leader election,Redis 不可用時排程失敗。→ 可接受,Redis 不可用時整個 worker 本就無法運作。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
直接部署,無資料遷移。首次執行會清理歷史殘留檔案,屬於預期行為。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
現有的檔案清理機制依賴 asynq 延遲任務(`file:remove`),在異常情況下(worker 重啟、任務丟失、Redis 資料不一致)可能導致本地磁碟殘留孤兒檔案,長期累積佔用儲存空間。需要一個每日定期掃描的兜底清理機制。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 新增 `file:janitor` asynq 任務處理函式(`worker/internal/tasks/janitor.go`)
|
||||||
|
- 在 `worker/main.go` 中加入 `asynq.Scheduler`,每天凌晨 00:00 自動排程 `file:janitor`
|
||||||
|
- 在 `worker/main.go` 的 mux 中註冊 `file:janitor` handler
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `file-janitor`: 每日定期掃描並清理三類漏網檔案:本地有但 fileInfoMap 無的孤兒檔、init 狀態已過期的未完成上傳、已完成上傳但無任何 share 關係的孤立檔
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **worker/internal/tasks/**:新增 `janitor.go`
|
||||||
|
- **worker/main.go**:新增 `asynq.Scheduler` 及 `file:janitor` handler 註冊
|
||||||
|
- 不影響任何 API 或前端
|
||||||
|
- 無新增外部依賴(`asynq.Scheduler` 已在現有依賴中)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 每日定期排程 file:janitor 任務
|
||||||
|
系統 SHALL 在每天凌晨 00:00(伺服器本地時間)自動排程一次 `file:janitor` asynq 任務。多個 worker 實例並存時,系統 SHALL 保證同一時間只有一個實例觸發排程(透過 asynq.Scheduler 的 Redis leader election 機制)。
|
||||||
|
|
||||||
|
#### Scenario: 單實例正常排程
|
||||||
|
- **WHEN** worker 啟動且時間到達每天 00:00
|
||||||
|
- **THEN** asynq.Scheduler 將 `file:janitor` 任務加入 asynq 佇列一次
|
||||||
|
|
||||||
|
#### Scenario: 多實例防重複觸發
|
||||||
|
- **WHEN** 多個 worker 實例同時運行且時間到達 00:00
|
||||||
|
- **THEN** 只有一個實例成功排程,其餘實例因 leader election 未獲鎖而跳過
|
||||||
|
|
||||||
|
### Requirement: 清理本地孤兒檔案(無 fileInfoMap 記錄)
|
||||||
|
系統 SHALL 掃描本地上傳目錄,對每個存在於磁碟但在 Redis `fileInfoMap` 中無對應記錄的檔案,直接執行 `os.RemoveAll` 刪除。
|
||||||
|
|
||||||
|
#### Scenario: 刪除孤兒本地檔案
|
||||||
|
- **WHEN** 本地上傳目錄中存在 fileId 目錄,且 Redis fileInfoMap 中無該 fileId 的記錄
|
||||||
|
- **THEN** 系統直接刪除該本地目錄
|
||||||
|
|
||||||
|
#### Scenario: 保留有 fileInfoMap 記錄的檔案
|
||||||
|
- **WHEN** 本地上傳目錄中存在 fileId 目錄,且 Redis fileInfoMap 中有該 fileId 的記錄
|
||||||
|
- **THEN** 系統不刪除該本地目錄,繼續處理下一個
|
||||||
|
|
||||||
|
### Requirement: 清理已過期的 init 狀態檔案
|
||||||
|
系統 SHALL 遍歷 Redis `fileInfoMap`,對 `FileType == "init"` 且滿足 `CreatedAt + Expire < 當前 Unix 時間戳` 的記錄,透過 `SetFileRemoveTask(fileId, 0)` 排程立即刪除。
|
||||||
|
|
||||||
|
#### Scenario: 排程刪除已過期 init 檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="init" 且 CreatedAt+Expire < now() 的記錄
|
||||||
|
- **THEN** 系統呼叫 SetFileRemoveTask(fileId, 0),將 file:remove 任務加入佇列
|
||||||
|
|
||||||
|
#### Scenario: 保留未過期的 init 檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="init" 且 CreatedAt+Expire >= now() 的記錄
|
||||||
|
- **THEN** 系統不排程刪除,繼續處理下一個
|
||||||
|
|
||||||
|
### Requirement: 清理無 share 關係的已完成上傳檔案
|
||||||
|
系統 SHALL 遍歷 Redis `fileInfoMap`,對 `FileType == "already"` 且在 Redis `fileShareRelational` 中無任何 share 關係的記錄,透過 `SetFileRemoveTask(fileId, 0)` 排程立即刪除。
|
||||||
|
|
||||||
|
#### Scenario: 排程刪除無 share 關係的已完成檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="already" 的記錄,且 fileShareRelational 中該 fileId 對應的 shareId 列表為空或不存在
|
||||||
|
- **THEN** 系統呼叫 SetFileRemoveTask(fileId, 0),將 file:remove 任務加入佇列
|
||||||
|
|
||||||
|
#### Scenario: 保留有 share 關係的已完成檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="already" 的記錄,且 fileShareRelational 中該 fileId 有一個或以上 shareId
|
||||||
|
- **THEN** 系統不排程刪除,繼續處理下一個
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## 1. 新增 FileJanitor 任務處理函式
|
||||||
|
|
||||||
|
- [x] 1.1 在 `worker/internal/tasks/file.go` 末尾新增 `FileJanitor(ctx context.Context, task *asynq.Task) error`(若加入後超過 300 行,則改建 `worker/internal/tasks/file/` 目錄,將 `RemoveFile` 移至 `remove.go`,`FileJanitor` 放至 `janitor.go`)
|
||||||
|
- [x] 1.2 在 `FileJanitor` 中呼叫 `u.GetUploadDirPath()` 取得本地上傳目錄路徑,並用 `os.ReadDir` 掃描目錄取得本地檔案清單
|
||||||
|
- [x] 1.3 呼叫 `models.GetRedisFileInfoAll()` 取得 fileInfoMap 全量資料,建立 fileId set
|
||||||
|
- [x] 1.4 實作 Case 1:本地存在但 fileInfoMap 無記錄的 fileId,呼叫 `os.RemoveAll(filePath)` 直接刪除
|
||||||
|
- [x] 1.5 遍歷 fileInfoMap,實作 Case 2:`FileType == "init"` 且 `CreatedAt + Expire < time.Now().Unix()`,呼叫 `pkgservices.SetFileRemoveTask(fileId, 0)`
|
||||||
|
- [x] 1.6 遍歷 fileInfoMap,實作 Case 3:`FileType == "already"` 且 `GetRedisFileShareRelational(fileId)` 回傳空 slice,呼叫 `pkgservices.SetFileRemoveTask(fileId, 0)`
|
||||||
|
|
||||||
|
## 2. 在 worker/main.go 中整合
|
||||||
|
|
||||||
|
- [x] 2.1 在 `mux` 中註冊 `mux.HandleFunc("file:janitor", tasks.FileJanitor)`
|
||||||
|
- [x] 2.2 建立 `asynq.NewScheduler`,使用與 `asynq.NewServer` 相同的 Redis 連線設定
|
||||||
|
- [x] 2.3 呼叫 `scheduler.Register("0 3 * * *", asynq.NewTask("file:janitor", nil))` 設定每日凌晨 03:00 排程
|
||||||
|
- [x] 2.4 在 `srv.Run(mux)` 之前啟動 `scheduler.Start()`,並在程式結束時呼叫 `scheduler.Shutdown()`
|
||||||
|
|
||||||
|
## 3. 驗證
|
||||||
|
|
||||||
|
- [x] 3.1 在 `worker/` 目錄下執行 `go build ./...`,確認編譯通過
|
||||||
45
openspec/specs/file-janitor/spec.md
Normal file
45
openspec/specs/file-janitor/spec.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 每日定期排程 file:janitor 任務
|
||||||
|
系統 SHALL 在每天凌晨 03:00(伺服器本地時間)自動排程一次 `file:janitor` asynq 任務。多個 worker 實例並存時,系統 SHALL 保證同一時間只有一個實例觸發排程(透過 asynq.Scheduler 的 Redis leader election 機制)。
|
||||||
|
|
||||||
|
#### Scenario: 單實例正常排程
|
||||||
|
- **WHEN** worker 啟動且時間到達每天 03:00
|
||||||
|
- **THEN** asynq.Scheduler 將 `file:janitor` 任務加入 asynq 佇列一次
|
||||||
|
|
||||||
|
#### Scenario: 多實例防重複觸發
|
||||||
|
- **WHEN** 多個 worker 實例同時運行且時間到達 03:00
|
||||||
|
- **THEN** 只有一個實例成功排程,其餘實例因 leader election 未獲鎖而跳過
|
||||||
|
|
||||||
|
### Requirement: 清理本地孤兒檔案(無 fileInfoMap 記錄)
|
||||||
|
系統 SHALL 掃描本地上傳目錄,對每個存在於磁碟但在 Redis `fileInfoMap` 中無對應記錄的資料夾,直接執行 `os.RemoveAll` 刪除。臨時上傳資料夾命名格式為 `<fileId>_tmp`,系統 SHALL 將 `_tmp` 後綴去除後再查詢 fileInfoMap,以判斷對應的 fileId 是否存在。
|
||||||
|
|
||||||
|
#### Scenario: 刪除孤兒本地資料夾
|
||||||
|
- **WHEN** 本地上傳目錄中存在名為 `<fileId>` 或 `<fileId>_tmp` 的資料夾,且 Redis fileInfoMap 中無該 fileId 的記錄
|
||||||
|
- **THEN** 系統直接刪除該本地資料夾(保留原始名稱,不去除 `_tmp`)
|
||||||
|
|
||||||
|
#### Scenario: 保留有 fileInfoMap 記錄的資料夾
|
||||||
|
- **WHEN** 本地上傳目錄中存在名為 `<fileId>` 或 `<fileId>_tmp` 的資料夾,且 Redis fileInfoMap 中有該 fileId 的記錄
|
||||||
|
- **THEN** 系統不刪除該資料夾,繼續處理下一個
|
||||||
|
|
||||||
|
### Requirement: 清理已過期的 init 狀態檔案
|
||||||
|
系統 SHALL 遍歷 Redis `fileInfoMap`,對 `FileType == "init"` 且滿足 `CreatedAt + Expire < 當前 Unix 時間戳` 的記錄,透過 `SetFileRemoveTask(fileId, 0)` 排程立即刪除。
|
||||||
|
|
||||||
|
#### Scenario: 排程刪除已過期 init 檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="init" 且 CreatedAt+Expire < now() 的記錄
|
||||||
|
- **THEN** 系統呼叫 SetFileRemoveTask(fileId, 0),將 file:remove 任務加入佇列
|
||||||
|
|
||||||
|
#### Scenario: 保留未過期的 init 檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="init" 且 CreatedAt+Expire >= now() 的記錄
|
||||||
|
- **THEN** 系統不排程刪除,繼續處理下一個
|
||||||
|
|
||||||
|
### Requirement: 清理無 share 關係的已完成上傳檔案
|
||||||
|
系統 SHALL 遍歷 Redis `fileInfoMap`,對 `FileType == "already"` 且在 Redis `fileShareRelational` 中無任何 share 關係的記錄,透過 `SetFileRemoveTask(fileId, 0)` 排程立即刪除。
|
||||||
|
|
||||||
|
#### Scenario: 排程刪除無 share 關係的已完成檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="already" 的記錄,且 fileShareRelational 中該 fileId 對應的 shareId 列表為空或不存在
|
||||||
|
- **THEN** 系統呼叫 SetFileRemoveTask(fileId, 0),將 file:remove 任務加入佇列
|
||||||
|
|
||||||
|
#### Scenario: 保留有 share 關係的已完成檔案
|
||||||
|
- **WHEN** fileInfoMap 中存在 FileType=="already" 的記錄,且 fileShareRelational 中該 fileId 有一個或以上 shareId
|
||||||
|
- **THEN** 系統不排程刪除,繼續處理下一個
|
||||||
@@ -1,22 +1,52 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
"github.com/redis/rueidis"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rdb rueidis.Client
|
var rdb rueidis.Client
|
||||||
|
|
||||||
|
const (
|
||||||
|
redisMaxRetries = 10
|
||||||
|
redisBaseDelay = 300 * time.Millisecond
|
||||||
|
redisBackoffFactor = 2.7
|
||||||
|
redisMaxDelay = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func InitRedis() error {
|
func InitRedis() error {
|
||||||
opt, err := rueidis.ParseURL(GetEnv("redis.url"))
|
opt, err := rueidis.ParseURL(GetEnv("redis.url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("invalid redis url: %w", err)
|
||||||
}
|
}
|
||||||
client, err := rueidis.NewClient(opt)
|
|
||||||
if err != nil {
|
var lastErr error
|
||||||
return err
|
for attempt := range redisMaxRetries {
|
||||||
|
var client rueidis.Client
|
||||||
|
client, lastErr = rueidis.NewClient(opt)
|
||||||
|
if lastErr == nil {
|
||||||
|
rdb = client
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if attempt == redisMaxRetries-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delay := time.Duration(math.Min(
|
||||||
|
float64(redisBaseDelay)*math.Pow(redisBackoffFactor, float64(attempt)),
|
||||||
|
float64(redisMaxDelay),
|
||||||
|
))
|
||||||
|
slog.Warn("redis connection failed, retrying",
|
||||||
|
"attempt", attempt+1,
|
||||||
|
"maxRetries", redisMaxRetries,
|
||||||
|
"retryIn", delay.String(),
|
||||||
|
"error", lastErr,
|
||||||
|
)
|
||||||
|
time.Sleep(delay)
|
||||||
}
|
}
|
||||||
rdb = client
|
return fmt.Errorf("redis connection failed after %d attempts: %w", redisMaxRetries, lastErr)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedisClient() rueidis.Client {
|
func GetRedisClient() rueidis.Client {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"pkg/models"
|
"pkg/models"
|
||||||
|
pkgservices "pkg/services"
|
||||||
u "pkg/utils"
|
u "pkg/utils"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
)
|
)
|
||||||
@@ -40,6 +43,10 @@ func RemoveFile(ctx context.Context, task *asynq.Task) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filePath := filepath.Join(uploadPath, payload.FileId)
|
filePath := filepath.Join(uploadPath, payload.FileId)
|
||||||
|
// 如果是临时文件删除文件夹
|
||||||
|
if fileInfo.FileType == models.FileTypeInit {
|
||||||
|
filePath += "_tmp"
|
||||||
|
}
|
||||||
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileInfoMap").Field(payload.FileId).Build()).Error(); err != nil {
|
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileInfoMap").Field(payload.FileId).Build()).Error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -48,3 +55,63 @@ func RemoveFile(ctx context.Context, task *asynq.Task) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FileJanitor(_ context.Context, _ *asynq.Task) error {
|
||||||
|
uploadPath, err := u.GetUploadDirPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(uploadPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allFileInfo, err := models.GetRedisFileInfoAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 1: 本地有但 fileInfoMap 無 → 直接刪除
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
fileId := strings.TrimSuffix(name, "_tmp")
|
||||||
|
if _, exists := allFileInfo[fileId]; !exists {
|
||||||
|
if err := os.RemoveAll(filepath.Join(uploadPath, name)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2 & 3: 遍歷 fileInfoMap
|
||||||
|
now := time.Now().Unix()
|
||||||
|
for fileId, rawInfo := range allFileInfo {
|
||||||
|
var info models.RedisFileInfo
|
||||||
|
if err := json.Unmarshal([]byte(rawInfo), &info); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: init 狀態且已過期
|
||||||
|
if info.FileType == models.FileTypeInit && info.CreatedAt+info.Expire < now {
|
||||||
|
if err := pkgservices.SetFileRemoveTask(fileId, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: 已完成上傳但無 share 關係
|
||||||
|
if info.FileType == models.FileTypeUpload {
|
||||||
|
shareIDs, err := models.GetRedisFileShareRelational(fileId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(shareIDs) == 0 {
|
||||||
|
if err := pkgservices.SetFileRemoveTask(fileId, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"pkg/geoip"
|
"pkg/geoip"
|
||||||
"pkg/models"
|
"pkg/models"
|
||||||
|
pkgservices "pkg/services"
|
||||||
u "pkg/utils"
|
u "pkg/utils"
|
||||||
"worker/internal/services"
|
"worker/internal/services"
|
||||||
|
|
||||||
@@ -35,21 +34,10 @@ func RemoveShare(ctx context.Context, task *asynq.Task) error {
|
|||||||
})
|
})
|
||||||
if len(shareIDs) == 0 {
|
if len(shareIDs) == 0 {
|
||||||
rdb := u.GetRedisClient()
|
rdb := u.GetRedisClient()
|
||||||
uploadPath, err := u.GetUploadDirPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filePath := filepath.Join(uploadPath, payload.FileId)
|
|
||||||
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileShareRelational").Field(payload.FileId).Build()).Error(); err != nil {
|
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileShareRelational").Field(payload.FileId).Build()).Error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileInfoMap").Field(payload.FileId).Build()).Error(); err != nil {
|
return pkgservices.SetFileRemoveTask(payload.FileId, 0)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(filePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if err := models.SetRedisFileShareRelational(payload.FileId, shareIDs); err != nil {
|
if err := models.SetRedisFileShareRelational(payload.FileId, shareIDs); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -46,10 +46,20 @@ func main() {
|
|||||||
mux.HandleFunc("share:remove", tasks.RemoveShare)
|
mux.HandleFunc("share:remove", tasks.RemoveShare)
|
||||||
mux.HandleFunc("share:notify", tasks.ShareNotify)
|
mux.HandleFunc("share:notify", tasks.ShareNotify)
|
||||||
mux.HandleFunc("file:remove", tasks.RemoveFile)
|
mux.HandleFunc("file:remove", tasks.RemoveFile)
|
||||||
|
mux.HandleFunc("file:janitor", tasks.FileJanitor)
|
||||||
mux.HandleFunc("image:compress", tasks.CompressImage)
|
mux.HandleFunc("image:compress", tasks.CompressImage)
|
||||||
mux.HandleFunc("image:convert", tasks.ConvertImage)
|
mux.HandleFunc("image:convert", tasks.ConvertImage)
|
||||||
mux.HandleFunc("text:translate", tasks.TranslateText)
|
mux.HandleFunc("text:translate", tasks.TranslateText)
|
||||||
|
|
||||||
|
scheduler := asynq.NewScheduler(utils.RedisURI2AsynqOpt(utils.GetEnv("redis.url")), nil)
|
||||||
|
if _, err := scheduler.Register("0 3 * * *", asynq.NewTask("file:janitor", nil)); err != nil {
|
||||||
|
log.Fatalf("could not register scheduler: %v", err)
|
||||||
|
}
|
||||||
|
if err := scheduler.Start(); err != nil {
|
||||||
|
log.Fatalf("could not start scheduler: %v", err)
|
||||||
|
}
|
||||||
|
defer scheduler.Shutdown()
|
||||||
|
|
||||||
if err := srv.Run(mux); err != nil {
|
if err := srv.Run(mux); err != nil {
|
||||||
log.Fatalf("could not run server: %v", err)
|
log.Fatalf("could not run server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user