mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 06:16:37 +08:00
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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -18,6 +19,11 @@ import (
|
||||
"github.com/ShukeBta/MediaStationGo/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
maxFFmpegZipEntryBytes = int64(2 << 30)
|
||||
maxFFmpegZipTotalBytes = int64(4 << 30)
|
||||
)
|
||||
|
||||
// AutoInstallFFmpeg is only called by the admin tool-install endpoint. The
|
||||
// server must not auto-download or keep ffmpeg/ffprobe running during startup.
|
||||
func AutoInstallFFmpeg(log *zap.Logger, cfg *config.Config) (ffprobePath, ffmpegPath string) {
|
||||
@@ -95,7 +101,7 @@ func downloadFFmpegWindows(log *zap.Logger, installDir string) (bool, error) {
|
||||
log.Info("开始下载 ffmpeg...")
|
||||
|
||||
// 创建安装目录
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(installDir, 0o750); err != nil {
|
||||
return false, fmt.Errorf("创建安装目录失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -175,7 +181,7 @@ func downloadFile(log *zap.Logger, url, filepath string) error {
|
||||
return fmt.Errorf("下载失败,HTTP 状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
out, err := os.Create(filepath)
|
||||
out, err := os.Create(filepath) // #nosec G304 -- filepath is generated by the installer under its temporary download directory.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,19 +191,98 @@ func downloadFile(log *zap.Logger, url, filepath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// unzip 解压 ZIP 文件 (简化版,实际应该使用 archive/zip)
|
||||
// unzip 解压 ZIP 文件。
|
||||
func unzip(log *zap.Logger, zipPath, destDir string) error {
|
||||
// Windows 使用 PowerShell 解压
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(destDir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
destRoot, err := filepath.Abs(destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
var totalWritten int64
|
||||
for _, file := range reader.File {
|
||||
if file.UncompressedSize64 > uint64(maxFFmpegZipEntryBytes) {
|
||||
return fmt.Errorf("zip entry too large: %s", file.Name)
|
||||
}
|
||||
target, err := safeZipTarget(destRoot, file.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info := file.FileInfo()
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
log.Warn("跳过 ZIP 符号链接", zap.String("name", file.Name))
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(target, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := info.Mode().Perm()
|
||||
if mode == 0 {
|
||||
mode = 0o644
|
||||
}
|
||||
dst, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) // #nosec G304 -- target is constrained by safeZipTarget to the extraction directory.
|
||||
if err != nil {
|
||||
_ = src.Close()
|
||||
return err
|
||||
}
|
||||
written, err := io.Copy(dst, io.LimitReader(src, maxFFmpegZipEntryBytes+1))
|
||||
totalWritten += written
|
||||
if err != nil {
|
||||
_ = dst.Close()
|
||||
_ = src.Close()
|
||||
return err
|
||||
}
|
||||
if written > maxFFmpegZipEntryBytes || totalWritten > maxFFmpegZipTotalBytes {
|
||||
_ = dst.Close()
|
||||
_ = src.Close()
|
||||
return fmt.Errorf("zip content too large: %s", file.Name)
|
||||
}
|
||||
if err := dst.Close(); err != nil {
|
||||
_ = src.Close()
|
||||
return err
|
||||
}
|
||||
if err := src.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command("powershell", "-Command", fmt.Sprintf("Expand-Archive -Path '%s' -DestinationPath '%s' -Force", zipPath, destDir))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
return fmt.Errorf("不支持的操作系统")
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeZipTarget(destRoot, name string) (string, error) {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\") || filepath.IsAbs(trimmed) {
|
||||
return "", fmt.Errorf("unsafe zip path: %s", name)
|
||||
}
|
||||
cleanName := filepath.Clean(strings.ReplaceAll(trimmed, "\\", "/"))
|
||||
if cleanName == "." || strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
|
||||
return "", fmt.Errorf("unsafe zip path: %s", name)
|
||||
}
|
||||
target := filepath.Join(destRoot, cleanName)
|
||||
targetAbs, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rel, err := filepath.Rel(destRoot, targetAbs)
|
||||
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
||||
return "", fmt.Errorf("unsafe zip path: %s", name)
|
||||
}
|
||||
return targetAbs, nil
|
||||
}
|
||||
|
||||
func findFFmpegPackageRoot(root string) (string, error) {
|
||||
@@ -268,17 +353,17 @@ func copyTree(srcPath, dstPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
in, err := os.Open(srcPath)
|
||||
in, err := os.Open(srcPath) // #nosec G304 -- srcPath is produced by walking the validated extracted ffmpeg package tree.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(dstPath)
|
||||
out, err := os.Create(dstPath) // #nosec G304 -- dstPath is generated under the configured ffmpeg install directory.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user