mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
114 lines
4.0 KiB
Go
114 lines
4.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// OrganizeSourceCandidate is a selectable organize source directory surfaced to
|
|
// the UI so operators can organize an arbitrary directory (such as the download
|
|
// directory) and not only registered libraries.
|
|
type OrganizeSourceCandidate struct {
|
|
Label string `json:"label"`
|
|
Path string `json:"path"`
|
|
Kind string `json:"kind"` // "download" | "media"
|
|
}
|
|
|
|
// OrganizeSourceCandidates returns the configured directories that are valid
|
|
// organize sources (download dir + media dir). It uses the container-visible
|
|
// paths; in NAS direct-read mode those equal the host paths the operator sees.
|
|
func (o *OrganizerService) OrganizeSourceCandidates(ctx context.Context) []OrganizeSourceCandidate {
|
|
out := []OrganizeSourceCandidate{}
|
|
seen := map[string]struct{}{}
|
|
add := func(label, path, kind string) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" || path == "." || strings.HasPrefix(path, ".") {
|
|
return
|
|
}
|
|
clean := filepath.Clean(path)
|
|
if !isAccessibleDir(clean) {
|
|
return
|
|
}
|
|
if _, ok := seen[clean]; ok {
|
|
return
|
|
}
|
|
seen[clean] = struct{}{}
|
|
out = append(out, OrganizeSourceCandidate{Label: label, Path: clean, Kind: kind})
|
|
}
|
|
add("默认整理源", o.settingValue(ctx, "organize.source_dir"), "source")
|
|
add("下载器保存目录", o.settingValue(ctx, "qbittorrent.savepath"), "download")
|
|
add("下载目录", envOrDefault("MEDIASTATION_DOWNLOAD_CONTAINER_DIR", "/downloads"), "download")
|
|
add("媒体目录", envOrDefault("MEDIASTATION_MEDIA_CONTAINER_DIR", "/media"), "media")
|
|
return out
|
|
}
|
|
|
|
func (o *OrganizerService) settingValue(ctx context.Context, key string) string {
|
|
if o.repo == nil || o.repo.Setting == nil {
|
|
return ""
|
|
}
|
|
if v, err := o.repo.Setting.Get(ctx, key); err == nil {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// defaultSourceRoot resolves the source root for a directory organize:
|
|
// explicit override → organize.source_dir setting → qB default save path →
|
|
// download container dir.
|
|
func (o *OrganizerService) defaultSourceRoot(ctx context.Context, override string) string {
|
|
if r := strings.TrimSpace(override); r != "" {
|
|
return r
|
|
}
|
|
if v := o.settingValue(ctx, "organize.source_dir"); v != "" {
|
|
return v
|
|
}
|
|
if v := o.settingValue(ctx, "qbittorrent.savepath"); v != "" {
|
|
return v
|
|
}
|
|
return envOrDefault("MEDIASTATION_DOWNLOAD_CONTAINER_DIR", "/downloads")
|
|
}
|
|
|
|
// defaultDestRoot resolves the destination root for a directory organize:
|
|
// explicit override → organize.target_dir setting → media container dir.
|
|
func (o *OrganizerService) defaultDestRoot(ctx context.Context, override string) string {
|
|
if r := strings.TrimSpace(override); r != "" {
|
|
return r
|
|
}
|
|
if o.repo != nil && o.repo.Setting != nil {
|
|
if v, err := o.repo.Setting.Get(ctx, "organize.target_dir"); err == nil && strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return envOrDefault("MEDIASTATION_MEDIA_CONTAINER_DIR", "/media")
|
|
}
|
|
|
|
func ensureOrganizeDestinationWritable(dest string) error {
|
|
dest = strings.TrimSpace(dest)
|
|
if dest == "" || dest == "." {
|
|
return errors.New("destination path required")
|
|
}
|
|
if _, ok := ParseCloudLibraryMount(dest); ok {
|
|
return errors.New("organize destination must be a local writable media directory; enable cloud transfer in external storage when writing to cloud")
|
|
}
|
|
if err := os.MkdirAll(dest, 0o755); err != nil { // #nosec G301 -- organized media directories must remain readable by NAS/player users.
|
|
return fmt.Errorf("destination path is not a writable directory: %s: %w", dest, err)
|
|
}
|
|
probe, err := os.CreateTemp(dest, ".mediastation-write-test-*") // #nosec G304 -- dest is operator-configured organize root.
|
|
if err != nil {
|
|
return fmt.Errorf("destination path is not writable: %s: %w", dest, err)
|
|
}
|
|
name := probe.Name()
|
|
if closeErr := probe.Close(); closeErr != nil {
|
|
_ = os.Remove(name)
|
|
return fmt.Errorf("destination path write probe failed: %s: %w", dest, closeErr)
|
|
}
|
|
if err := os.Remove(name); err != nil {
|
|
return fmt.Errorf("destination path cleanup probe failed: %s: %w", dest, err)
|
|
}
|
|
return nil
|
|
}
|