Files
MeBox/internal/service/transfer.go
T
ShukeBta 62b204367c fix: auto-scan cloud libraries on boot + allow CORS for media playback
- Add BootCloudLibraries() to auto-scan all cloud libraries on startup
- Delay 3s to avoid conflict with system init, scan without auto-scrape
- Enable CORS for /api/cloud/play/* and /api/img to support 3rd-party players
- Fixes issue where each user triggers separate cloud library scans
- Fixes issue where Infuse/Emby apps cannot play cloud resources
2026-06-11 11:25:43 +08:00

98 lines
3.2 KiB
Go

// Package service — file transfer strategies for the organizer.
//
// 整理媒体时支持四种转移方式:
//
// move 移动(同盘 rename,跨盘 copy+删除源)——会移除源文件
// copy 复制(保留源文件)
// hardlink 硬链接(同盘零额外占用,保留源文件;做种不受影响)
// symlink 软链接(保留源文件,指向源)
//
// 除 move 外,其余方式都保留源文件,因此 qBittorrent 等下载器仍能在原
// 路径找到数据继续做种上传。
package service
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TransferMode 表示整理时文件的转移方式。
type TransferMode string
const (
// TransferMove 移动:同盘 rename,跨盘 copy+删除源。
TransferMove TransferMode = "move"
// TransferCopy 复制:保留源文件。
TransferCopy TransferMode = "copy"
// TransferHardlink 硬链接:同盘零额外占用并保留源文件,做种不受影响。
TransferHardlink TransferMode = "hardlink"
// TransferSymlink 软链接:保留源文件,目标指向源。
TransferSymlink TransferMode = "symlink"
)
// parseTransferMode 解析转移方式字符串,无法识别时回退为默认的移动。
func parseTransferMode(s string) TransferMode {
switch strings.ToLower(strings.TrimSpace(s)) {
case "copy", "复制":
return TransferCopy
case "hardlink", "hard", "link", "硬链接", "硬连接":
return TransferHardlink
case "symlink", "soft", "softlink", "软链接", "软连接", "符号链接":
return TransferSymlink
default:
return TransferMove
}
}
// keepsSource 报告该转移方式是否会保留源文件(用于做种)。
func (m TransferMode) keepsSource() bool {
return m == TransferCopy || m == TransferHardlink || m == TransferSymlink
}
// transferFile 按指定方式把 src 转移到 dst。
// dst 已存在时一律报错,绝不覆盖(防止不同 release 改名后互相覆盖)。
func transferFile(src, dst string, mode TransferMode) error {
if _, err := os.Stat(dst); err == nil {
return fmt.Errorf("destination already exists: %s", dst)
}
switch mode {
case TransferCopy:
return copyFile(src, dst)
case TransferHardlink:
if err := os.Link(src, dst); err != nil {
return fmt.Errorf("hardlink failed: %w; source and target must be on the same filesystem, choose copy if you want to duplicate data", err)
}
return nil
case TransferSymlink:
target := src
if abs, err := filepath.Abs(src); err == nil {
target = abs
}
return os.Symlink(target, dst)
default: // TransferMove
return moveFile(src, dst)
}
}
// copyFile 流式复制 src 到 dst(保留源文件)。O_EXCL 保证不覆盖已存在目标。
func copyFile(src, dst string) error {
in, err := os.Open(src) // #nosec G304 -- src is selected from configured media/download roots by the organizer.
if err != nil {
return err
}
defer in.Close()
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) // #nosec G304,G302 -- dst is organizer-generated; media files must remain readable by local players.
if err != nil {
return err
}
if _, werr := io.Copy(f, in); werr != nil {
_ = f.Close()
_ = os.Remove(dst)
return werr
}
return f.Close()
}