package jobs import ( "fmt" "os" "path/filepath" "strings" "ddc/internal/explain" "ddc/internal/files" "ddc/internal/libraries" "ddc/internal/preferences" ) type moveTarget struct { destination string subfolder string } func (target *moveTarget) displayPath() string { return filepath.Join(target.destination, target.subfolder) } func (manager *Manager) resolveTarget(destination, subfolder string) (*moveTarget, error) { if strings.TrimSpace(destination) == "" { return nil, nil } libraryPath, extraFolder, err := manager.splitDownloadPath(destination) if err != nil { return nil, invalid(err) } cleanSubfolder, err := files.SanitizeSubfolder(subfolder) if err != nil { return nil, invalid(err) } combinedSubfolder, err := files.SanitizeSubfolder(extraFolder + "/" + cleanSubfolder) if err != nil { return nil, invalid(err) } return &moveTarget{destination: libraryPath, subfolder: combinedSubfolder}, nil } func (manager *Manager) splitDownloadPath(fullPath string) (string, string, error) { cleaned := filepath.Clean(strings.TrimSpace(fullPath)) for _, root := range manager.settings.LibraryRoots { if !files.IsWithin(root, cleaned) { continue } relative, err := filepath.Rel(root, cleaned) if err != nil { return "", "", err } libraryName, extraFolder, _ := strings.Cut(filepath.ToSlash(relative), "/") library, err := libraries.Resolve(manager.settings.LibraryRoots, filepath.Join(root, libraryName)) if err != nil { return "", "", err } return library.Path, extraFolder, nil } return "", "", libraries.ErrUnknownDestination } func (manager *Manager) normalizeDownloadPaths(downloadPaths []string) ([]string, error) { normalized := make([]string, 0, len(downloadPaths)) seenPaths := make(map[string]bool) for _, downloadPath := range downloadPaths { target, err := manager.resolveTarget(downloadPath, "") if err != nil || target == nil { return nil, fmt.Errorf("%w: %q is not inside a destination folder", preferences.ErrInvalid, downloadPath) } fullPath := target.displayPath() if !seenPaths[fullPath] { seenPaths[fullPath] = true normalized = append(normalized, fullPath) } } return normalized, nil } func (manager *Manager) moveToTarget(job *Job) { if job.target == nil { return } names, err := topLevelNames(job.directory) if err == nil { job.logger().Info("moving to chosen folder", "destination", job.target.displayPath(), "items", len(names)) err = manager.Move(job.id, MoveRequest{ Paths: names, Destination: job.target.destination, Subfolder: job.target.subfolder, Cleanup: true, }) } if err != nil { job.logger().Error("automatic move failed", "destination", job.target.displayPath(), "error", err) job.setStatus(StatusReady, "Move failed: "+explain.Error(err)) } } func topLevelNames(directory string) ([]string, error) { entries, err := os.ReadDir(directory) if err != nil { return nil, err } names := make([]string, 0, len(entries)) for _, entry := range entries { if !files.IsInternal(entry.Name()) { names = append(names, entry.Name()) } } return names, nil }