Files
LitePan/internal/api/cache_retention_admin.go
T
2026-07-27 19:31:43 +08:00

323 lines
9.3 KiB
Go

package api
import (
"net/http"
"strconv"
"time"
"litepan/internal/cacheretention"
"litepan/internal/domain"
)
type retentionTaskDTO struct {
ID int64 `json:"id,omitempty"`
AccountID int64 `json:"account_id"`
AccountName string `json:"account_name,omitempty"`
ParentID string `json:"parent_id"`
Path string `json:"path"`
ScanDepth int `json:"scan_depth"`
ApiInterval int `json:"api_interval"`
RefreshInterval int `json:"refresh_interval"`
Status string `json:"status"`
PausedReason string `json:"paused_reason,omitempty"`
FileCount int `json:"file_count"`
LastRefresh string `json:"last_refresh,omitempty"`
LastRefreshStatus string `json:"last_refresh_status,omitempty"`
LastDurationMs int `json:"last_duration_ms"`
LastAPICalls int `json:"last_api_calls"`
LastSkipCalls int `json:"last_skip_calls"`
LastScannedDirs int `json:"last_scanned_dirs"`
ErrorMessage string `json:"error_message,omitempty"`
TimeWindowEnabled bool `json:"time_window_enabled"`
TimeStart string `json:"time_start"`
TimeEnd string `json:"time_end"`
ScannedDirs int `json:"scanned_dirs,omitempty"`
ScannedFiles int `json:"scanned_files,omitempty"`
CurrentDir string `json:"current_dir,omitempty"`
StartedAt string `json:"started_at,omitempty"`
CurrentDurationMs int `json:"current_duration_ms,omitempty"`
IsRunning bool `json:"is_running"`
IsPending bool `json:"is_pending"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
func toRetentionDTO(t *domain.CacheRetentionTask, svc *cacheretention.Service) retentionTaskDTO {
if t == nil {
return retentionTaskDTO{}
}
dto := retentionTaskDTO{
ID: t.ID,
AccountID: t.AccountID,
AccountName: t.AccountName,
ParentID: t.ParentID,
Path: t.Path,
ScanDepth: t.ScanDepth,
ApiInterval: t.ApiInterval,
RefreshInterval: t.RefreshInterval,
Status: t.Status,
PausedReason: t.PausedReason,
FileCount: t.FileCount,
LastRefreshStatus: t.LastRefreshStatus,
LastDurationMs: t.LastDurationMS,
LastAPICalls: t.LastAPICalls,
LastSkipCalls: t.LastSkipCalls,
LastScannedDirs: t.LastScannedDirs,
ErrorMessage: t.ErrorMessage,
TimeWindowEnabled: t.TimeWindowEnabled,
TimeStart: t.TimeStart,
TimeEnd: t.TimeEnd,
IsRunning: svc != nil && svc.IsExecuting(t.ID),
IsPending: svc != nil && svc.IsPending(t.ID),
CreatedAt: FormatAPITime(t.CreatedAt),
UpdatedAt: FormatAPITime(t.UpdatedAt),
}
if t.LastRefresh != nil {
dto.LastRefresh = FormatAPITime(*t.LastRefresh)
}
if svc != nil {
if st, ok := svc.TaskLiveStats(t.ID); ok {
dto.ScannedDirs = st.ScannedDirs
dto.ScannedFiles = st.ScannedFiles
dto.CurrentDir = st.CurrentDir
if !st.StartedAt.IsZero() {
dto.StartedAt = FormatAPITime(st.StartedAt)
dto.CurrentDurationMs = int(time.Since(st.StartedAt).Milliseconds())
}
}
}
return dto
}
func fromRetentionDTO(d retentionTaskDTO) *domain.CacheRetentionTask {
return &domain.CacheRetentionTask{
ID: d.ID,
AccountID: d.AccountID,
ParentID: d.ParentID,
Path: d.Path,
ScanDepth: d.ScanDepth,
ApiInterval: d.ApiInterval,
RefreshInterval: d.RefreshInterval,
TimeWindowEnabled: d.TimeWindowEnabled,
TimeStart: d.TimeStart,
TimeEnd: d.TimeEnd,
}
}
func retentionRunPayload(result cacheretention.RunNowResult) map[string]any {
return map[string]any{
"state": result.State,
"startup_remaining": result.StartupRemaining,
"retry_after_seconds": result.RetryAfterSeconds,
"cache_ttl_minutes": result.CacheTTLMinutes,
}
}
func retentionRunMessage(result cacheretention.RunNowResult, create bool) string {
if create {
switch result.State {
case "queued_startup":
return "配置创建成功,启动退避结束后(约 " + strconv.Itoa(result.StartupRemaining) + " 秒)自动执行"
case "blocked_by_strm":
return "配置创建成功,同账号任务占用结束后自动执行"
case "running":
return "配置创建成功,已触发执行"
case "cache_disabled":
return "配置创建成功,但该账号目录缓存已关闭,任务无法生效"
default:
return "配置创建成功"
}
}
return result.Message()
}
func (h *Handler) listRetentionTasks(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
tasks, err := h.cacheRetention.ListTasks(r.Context())
if err != nil {
writeErr(w, err)
return
}
out := make([]retentionTaskDTO, 0, len(tasks))
for _, t := range tasks {
out = append(out, toRetentionDTO(t, h.cacheRetention))
}
writeJSON(w, http.StatusOK, Resp{
Success: true,
Data: map[string]any{
"items": out,
"startup_remaining": h.cacheRetention.StartupRemaining(),
},
})
}
func (h *Handler) getRetentionStats(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
stats, err := h.cacheRetention.Stats(r.Context())
if err != nil {
writeErr(w, err)
return
}
writeOK(w, stats)
}
func (h *Handler) createRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
var in retentionTaskDTO
if err := decodeJSON(r, &in); err != nil {
writeErr(w, err)
return
}
task, err := h.cacheRetention.CreateTask(r.Context(), fromRetentionDTO(in))
if err != nil {
writeErr(w, err)
return
}
run := h.cacheRetention.RunNow(r.Context(), task.ID)
writeJSON(w, http.StatusOK, Resp{
Success: true,
Data: map[string]any{"id": task.ID, "run": retentionRunPayload(run)},
Message: retentionRunMessage(run, true),
})
}
func (h *Handler) updateRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
var in retentionTaskDTO
if err := decodeJSON(r, &in); err != nil {
writeErr(w, err)
return
}
in.ID = id
task, err := h.cacheRetention.UpdateTask(r.Context(), fromRetentionDTO(in))
if err != nil {
writeErr(w, err)
return
}
writeOK(w, toRetentionDTO(task, h.cacheRetention))
}
func (h *Handler) deleteRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
if err := h.cacheRetention.DeleteTask(r.Context(), id); err != nil {
writeErr(w, err)
return
}
writeJSON(w, http.StatusOK, Resp{Success: true, Message: "配置已删除"})
}
func (h *Handler) toggleRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
task, err := h.cacheRetention.ToggleTask(r.Context(), id)
if err != nil {
writeErr(w, err)
return
}
writeJSON(w, http.StatusOK, Resp{Success: true, Data: toRetentionDTO(task, h.cacheRetention), Message: "状态已切换"})
}
func (h *Handler) refreshRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
run := h.cacheRetention.RunNow(r.Context(), id)
if run.State == "missing" {
writeJSON(w, http.StatusNotFound, Resp{Success: false, Message: "配置不存在"})
return
}
writeJSON(w, http.StatusOK, Resp{
Success: true,
Data: retentionRunPayload(run),
Message: retentionRunMessage(run, false),
})
}
func (h *Handler) forceStopRetentionTask(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
if err := h.cacheRetention.ForceStop(r.Context(), id); err != nil {
writeErr(w, err)
return
}
writeJSON(w, http.StatusOK, Resp{Success: true, Message: "任务已强制停止,下次调度不受影响"})
}
func (h *Handler) ackRetentionScopeWarn(w http.ResponseWriter, r *http.Request) {
if !ensureServiceReady(w, h.cacheRetention != nil) {
return
}
id, err := pathID(r)
if err != nil {
writeErr(w, err)
return
}
if err := h.cacheRetention.AckLargeScopeWarn(r.Context(), id); err != nil {
writeErr(w, err)
return
}
if h.notifications != nil {
_, _ = h.notifications.DeleteByRef(r.Context(), domain.NotificationCategoryCacheScopeWarn, id)
}
writeJSON(w, http.StatusOK, Resp{Success: true, Message: "已关闭该任务的范围过大提醒"})
}
func (h *Handler) retentionDefaults(w http.ResponseWriter, _ *http.Request) {
remaining := 0
if h.cacheRetention != nil {
remaining = h.cacheRetention.StartupRemaining()
}
writeOK(w, map[string]any{
"api_interval": 200,
"refresh_interval": 60,
"scan_depth": 4,
"max_configs": 6,
"startup_remaining": remaining,
})
}
func (h *Handler) retentionStartupRemaining(w http.ResponseWriter, _ *http.Request) {
remaining := 0
if h.cacheRetention != nil {
remaining = h.cacheRetention.StartupRemaining()
}
writeOK(w, map[string]any{"startup_remaining": remaining})
}