Files
MeBox/internal/service/organizer_directory_sources.go
T

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
}