mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
585edeb984
资源占用(Docker 部署 CPU/内存长期居高): - 云盘探测预算改为按尝试扣减,杜绝队列满时对每个文件反复入队 并刷出数万条 WARN(实测日志 41165 条) - 探测队列满时给文件挂 30 分钟退避 + 告警限速为每分钟一条 - 扫描时每个文件的海报/背景图由同步下载(单张最长 20s)改为 后台预取队列,云盘大库扫描不再串行拉图数小时 - PlaybackInfo 的云盘 ffprobe 探测改异步(原同步最长 8s, 既拖慢起播又放大云盘流量),带单飞去重 - 访问日志跳过 /api/health 与静态资源;logging.level/format 配置真正生效(此前是死配置) 登录稳定性(经常登录报错): - refresh token 未及时落库期间,刷新请求可识别「待落库令牌」, 不再把用户踢回登录页;轮换/登出后取消后台补写,防止旧令牌复活 QB 下载整理入库: - 新增 download.path_mappings 设置:自定义下载器→本程序路径映射 (每行 客户端路径=本地路径),并复用 compose 环境变量映射规则 - 应用重启后补整理最近 24h 内完成的种子(此前重启即永久漏掉) - 下载客户端初始化失败仍注册并惰性重连(容器启动顺序免疫) - 硬链接跨文件系统(EXDEV)自动降级为复制,保种语义不变 第三方播放器 404: - 播放处理器不再把所有错误吞成 404:媒体不存在→404, 云盘解析失败/STRM 关闭→502+原因 - 存库的云盘播放 URL 规范化为相对路径,免疫扫描时固化的旧 host - 云盘媒体 SupportsDirectPlay=false,强制走带鉴权的 DirectStream
105 lines
3.6 KiB
Go
105 lines
3.6 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 {
|
|
// Docker 部署里下载目录和媒体目录往往是两个独立的 bind mount,
|
|
// 即使在宿主机上同属一块盘,容器内 os.Link 也会因跨文件系统
|
|
// (EXDEV) 失败。此前直接报错导致 PT 下载完成后整理静默中断;
|
|
// 现在自动降级为复制(保留源文件继续做种,语义一致)。
|
|
if copyErr := copyFile(src, dst); copyErr == nil {
|
|
return 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()
|
|
}
|