Files
2026-07-30 17:09:53 +08:00

433 lines
13 KiB
Go

package file
import (
"context"
"encoding/json"
"log/slog"
"strconv"
"strings"
"time"
"litepan/internal/cache"
"litepan/internal/core/driverexec"
"litepan/internal/domain"
"litepan/internal/driver"
"litepan/internal/eventbus"
"litepan/internal/settings"
)
// defaultDirTTL 是 settings 不可用(如测试)时的目录缓存兜底时长。
const defaultDirTTL = 30 * time.Minute
// Service 跨驱动文件浏览;读走缓存,写后发 FileMutated。
type Service struct {
exec *driverexec.Executor
cache *cache.Service
accounts domain.AccountRepository
bus *eventbus.Bus
settings *settings.Service
listHits *cache.HitTracker
log *slog.Logger
}
func NewService(exec *driverexec.Executor, c *cache.Service, accounts domain.AccountRepository, bus *eventbus.Bus, set *settings.Service, listHits *cache.HitTracker) *Service {
return &Service{exec: exec, cache: c, accounts: accounts, bus: bus, settings: set, listHits: listHits, log: slog.Default()}
}
// SetLogger 装配期注入 file_op 模块 logger;不调用则回落 slog.Default。
func (s *Service) SetLogger(log *slog.Logger) {
if log != nil {
s.log = log
}
}
// List 列举某账号下 parentID 目录的子项。forceRefresh 时跳过并失效该目录缓存。
func (s *Service) List(ctx context.Context, accountID int64, parentID string, forceRefresh bool) ([]domain.FileItem, error) {
parentID = cache.NormalizeDirParentID(parentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return nil, err
}
if s.cache == nil {
return s.listFromDriver(ctx, accountID, parentID)
}
ttl := s.dirTTL(ctx, accountID)
if forceRefresh {
cache.InvalidateDirKeys(s.cache, accountID, parentID)
}
if ttl <= 0 || s.cache.DirIsCooling(accountID, parentID) {
return s.listFromDriver(ctx, accountID, parentID)
}
key := cache.DirKey(accountID, parentID)
items, hit, err := cache.GetOrLoadAs[cache.DirList](ctx, s.cache, key, ttl, func(callCtx context.Context) (cache.DirList, error) {
if s.cache.DirIsCooling(accountID, parentID) {
return s.listFromDriver(callCtx, accountID, parentID)
}
var list cache.DirList
lerr := s.exec.Run(callCtx, accountID, func(drv driver.Driver) error {
got, err := drv.ListFiles(callCtx, parentID)
if err != nil {
return err
}
list = got
return nil
})
if lerr != nil {
return nil, lerr
}
return list, nil
})
if err != nil {
return nil, err
}
if hit {
s.recordListHit(true)
} else {
s.recordListHit(false)
}
return items, nil
}
func (s *Service) listFromDriver(ctx context.Context, accountID int64, parentID string) ([]domain.FileItem, error) {
var items []domain.FileItem
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
got, err := drv.ListFiles(ctx, parentID)
if err != nil {
return err
}
items = got
return nil
})
if err == nil {
s.recordListHit(false)
}
return items, err
}
// WarmAccount 挂载/联调前预热账号:强制刷新根目录列表,初始化驱动实例并触发 OAuth 被动续期。
func (s *Service) WarmAccount(ctx context.Context, accountID int64) error {
_, err := s.List(ctx, accountID, "", true)
return err
}
// Info 获取单文件信息:驱动未实现该能力时按 NOT_IMPLEMENT 降级。
func (s *Service) Info(ctx context.Context, accountID int64, fileID string) (*domain.FileItem, error) {
if err := s.exec.Check(ctx, accountID); err != nil {
return nil, err
}
if s.cache == nil {
return s.infoFromDriver(ctx, accountID, fileID)
}
ttl := s.dirTTL(ctx, accountID)
if ttl <= 0 {
return s.infoFromDriver(ctx, accountID, fileID)
}
key := cache.FileInfoKey(accountID, fileID)
info, _, err := cache.GetOrLoadAs[cache.FileInfo](ctx, s.cache, key, ttl, func(callCtx context.Context) (cache.FileInfo, error) {
item, err := s.infoFromDriver(callCtx, accountID, fileID)
if err != nil {
return cache.FileInfo{}, err
}
return *item, nil
})
if err != nil {
return nil, err
}
item := info
return &item, nil
}
func (s *Service) infoFromDriver(ctx context.Context, accountID int64, fileID string) (*domain.FileItem, error) {
var item *domain.FileItem
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
g, ok := drv.(driver.InfoGetter)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
got, err := g.GetFileInfo(ctx, fileID)
if err != nil {
return err
}
item = got
return nil
})
return item, err
}
func (s *Service) DeleteFiles(ctx context.Context, accountID int64, fileIDs []string, parentID string) error {
parentID = cache.NormalizeDirParentID(parentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return err
}
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
deleter, ok := drv.(driver.Deleter)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
return deleter.DeleteFiles(ctx, fileIDs)
})
if err != nil {
s.log.Warn("删除文件失败", "account_id", accountID, "count", len(fileIDs), "err", err)
return err
}
s.log.Debug("删除文件成功", "account_id", accountID, "count", len(fileIDs), "parent_id", parentID)
s.publishMutation(ctx, eventbus.FileMutated{AccountID: accountID, Op: "delete", ParentID: parentID, FileIDs: fileIDs})
return nil
}
func (s *Service) MoveFiles(ctx context.Context, accountID int64, fileIDs []string, targetParentID, sourceParentID string) error {
targetParentID = cache.NormalizeDirParentID(targetParentID)
sourceParentID = cache.NormalizeDirParentID(sourceParentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return err
}
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
mover, ok := drv.(driver.Mover)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
return mover.MoveFiles(ctx, fileIDs, targetParentID, sourceParentID)
})
if err != nil {
s.log.Warn("移动文件失败", "account_id", accountID, "count", len(fileIDs), "err", err)
return err
}
s.log.Debug("移动文件成功", "account_id", accountID, "count", len(fileIDs), "source", sourceParentID, "target", targetParentID)
s.publishMutation(ctx, eventbus.FileMutated{
AccountID: accountID,
Op: "move",
ParentID: targetParentID,
OldParentID: sourceParentID,
FileIDs: fileIDs,
})
return nil
}
func (s *Service) CopyFiles(ctx context.Context, accountID int64, fileIDs []string, targetParentID string) error {
targetParentID = cache.NormalizeDirParentID(targetParentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return err
}
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
copier, ok := drv.(driver.Copier)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
return copier.CopyFiles(ctx, fileIDs, targetParentID)
})
if err != nil {
s.log.Warn("复制文件失败", "account_id", accountID, "count", len(fileIDs), "err", err)
return err
}
s.log.Debug("复制文件成功", "account_id", accountID, "count", len(fileIDs), "target", targetParentID)
s.publishMutation(ctx, eventbus.FileMutated{AccountID: accountID, Op: "copy", ParentID: targetParentID})
return nil
}
func (s *Service) RenameFile(ctx context.Context, accountID int64, fileID, newName, parentID string) error {
parentID = cache.NormalizeDirParentID(parentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return err
}
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
renamer, ok := drv.(driver.Renamer)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
return renamer.RenameFile(ctx, fileID, newName)
})
if err != nil {
s.log.Warn("重命名失败", "account_id", accountID, "file_id", fileID, "new_name", newName, "err", err)
return err
}
s.log.Debug("重命名成功", "account_id", accountID, "file_id", fileID, "new_name", newName)
s.publishMutation(ctx, eventbus.FileMutated{AccountID: accountID, Op: "rename", ParentID: parentID, FileID: fileID})
return nil
}
func (s *Service) CreateFolder(ctx context.Context, accountID int64, parentID, name string) (*domain.FileItem, error) {
parentID = cache.NormalizeDirParentID(parentID)
if err := s.exec.Check(ctx, accountID); err != nil {
return nil, err
}
var item *domain.FileItem
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
creator, ok := drv.(driver.FolderCreator)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
got, err := creator.CreateFolder(ctx, parentID, name)
if err != nil {
return err
}
item = got
return nil
})
if err != nil {
s.log.Warn("创建文件夹失败", "account_id", accountID, "name", name, "err", err)
return nil, err
}
s.log.Debug("创建文件夹成功", "account_id", accountID, "name", name, "parent_id", parentID)
s.publishMutation(ctx, eventbus.FileMutated{
AccountID: accountID,
Op: "create",
ParentID: parentID,
FileID: item.ID,
FileName: item.Name,
FileSize: item.Size,
IsDir: item.IsDir,
ModTime: item.ModTime,
})
return item, nil
}
// UploadLocal 从服务器本地临时文件上传到网盘;成功后同步失效父目录缓存并发布 FileMutated。
func (s *Service) UploadLocal(ctx context.Context, accountID int64, req driver.LocalUploadRequest) (*driver.LocalUploadResult, error) {
if err := s.exec.Check(ctx, accountID); err != nil {
return nil, err
}
var result *driver.LocalUploadResult
err := s.exec.Run(ctx, accountID, func(drv driver.Driver) error {
uploader, ok := drv.(driver.LocalUploader)
if !ok {
return domain.Errf(domain.CodeNotImplement)
}
r, err := uploader.UploadLocalFile(ctx, req)
if err != nil {
return err
}
result = r
return nil
})
if err != nil {
s.log.Warn("上传文件失败", "account_id", accountID, "name", req.FileName, "err", err)
return nil, err
}
s.log.Debug("上传文件成功", "account_id", accountID, "name", result.FileName, "size", result.Size)
parentID := cache.NormalizeDirParentID(req.ParentID)
if result.ParentID != "" {
parentID = cache.NormalizeDirParentID(result.ParentID)
}
mut := eventbus.FileMutated{
AccountID: accountID,
Op: "create",
ParentID: parentID,
FileID: result.FileID,
FileName: result.FileName,
FileSize: result.Size,
}
if req.ModTime != nil && !req.ModTime.IsZero() {
mut.ModTime = *req.ModTime
}
s.publishMutation(ctx, mut)
return result, nil
}
// NotifyCreated 用于绕过 file.Service 写路径完成创建时,补发标准写后失效事件。
// 例如跨盘秒传命中后由驱动直接落文件,需要调用此方法清理目标目录缓存。
func (s *Service) NotifyCreated(ctx context.Context, accountID int64, parentID, fileID, fileName string, fileSize int64, isDir bool) {
s.publishMutation(ctx, eventbus.FileMutated{
AccountID: accountID,
Op: "create",
ParentID: parentID,
FileID: fileID,
FileName: fileName,
FileSize: fileSize,
IsDir: isDir,
})
}
func (s *Service) recordListHit(hit bool) {
if s.listHits == nil {
return
}
if hit {
s.listHits.RecordHit()
} else {
s.listHits.RecordMiss()
}
}
// DirCacheTTL 返回目录/WebDAV 路径缓存 TTL。
func (s *Service) DirCacheTTL(ctx context.Context, accountID int64) time.Duration {
return s.dirTTL(ctx, accountID)
}
// 先同步失效再发事件,避免写后重列命中旧缓存。
func (s *Service) publishMutation(ctx context.Context, e eventbus.FileMutated) {
e.ParentID = cache.NormalizeDirParentID(e.ParentID)
e.OldParentID = cache.NormalizeDirParentID(e.OldParentID)
if s.cache != nil {
cache.ApplyMutation(s.cache, e)
}
if s.bus != nil {
s.bus.Publish(ctx, e)
}
}
// TTL:全局关>账号 cache_ttl>全局默认;账号 0=禁用。
func (s *Service) dirTTL(ctx context.Context, accountID int64) time.Duration {
if s.settings != nil && !s.settings.Bool(settings.KeyCacheEnabled) {
return 0
}
if s.accounts != nil {
if acc, err := s.accounts.Get(ctx, accountID); err == nil && acc != nil {
if minutes, ok := parseCacheTTLMinutes(acc.Config); ok {
if minutes <= 0 {
return 0
}
return time.Duration(minutes) * time.Minute
}
}
}
return s.globalDirTTL()
}
// globalDirTTL 返回全局默认目录缓存时长;settings 不可用时回落兜底常量。
func (s *Service) globalDirTTL() time.Duration {
if s.settings == nil {
return defaultDirTTL
}
minutes := s.settings.Int(settings.KeyCacheTTL)
if minutes <= 0 {
return 0
}
return time.Duration(minutes) * time.Minute
}
func parseCacheTTLMinutes(configJSON string) (int, bool) {
s := strings.TrimSpace(configJSON)
if s == "" || s == "{}" {
return 0, false
}
var m map[string]json.RawMessage
if err := json.Unmarshal([]byte(s), &m); err != nil {
return 0, false
}
raw, ok := m["cache_ttl"]
if !ok {
return 0, false
}
var num json.Number
if err := json.Unmarshal(raw, &num); err == nil {
if n, err := num.Int64(); err == nil {
return int(n), true
}
}
var str string
if err := json.Unmarshal(raw, &str); err == nil {
str = strings.TrimSpace(str)
if str == "" {
return 0, false
}
if n, err := strconv.Atoi(str); err == nil {
return n, true
}
}
return 0, false
}