package jobs import ( "errors" "fmt" "os" "path/filepath" "time" "ddc/internal/explain" "ddc/internal/files" ) type MoveRequest struct { Paths []string `json:"paths"` Destination string `json:"destination"` Subfolder string `json:"subfolder"` Cleanup bool `json:"cleanup"` } func (manager *Manager) Move(id string, request MoveRequest) error { job, err := manager.lookup(id) if err != nil { return err } if job.currentStatus() != StatusReady { return ErrJobBusy } if len(request.Paths) == 0 { return invalid(errors.New("select at least one file or folder")) } target, err := manager.resolveTarget(request.Destination, request.Subfolder) if err != nil { return err } if target == nil { return invalid(errors.New("choose a destination")) } sources, err := resolveSources(job.directory, request.Paths) if err != nil { return err } destination, err := files.EnsureSubdirectory(target.destination, target.subfolder) if err != nil { return invalid(err) } if err := checkTargets(sources, destination); err != nil { return err } if !job.begin(StatusMoving, StatusReady) { return ErrJobBusy } manager.runInBackground(job, "move", func() { manager.moveFiles(job, sources, destination, request.Cleanup) }) return nil } func resolveSources(root string, relativePaths []string) ([]string, error) { sources := make([]string, 0, len(relativePaths)) seenNames := make(map[string]bool) for _, relative := range relativePaths { source, err := files.ResolveInside(root, relative) if err != nil { return nil, invalid(err) } name := filepath.Base(source) if files.IsInternal(name) { return nil, invalid(files.ErrUnsafePath) } if seenNames[name] { return nil, invalid(fmt.Errorf("more than one selected item is named %q", name)) } seenNames[name] = true sources = append(sources, source) } for _, outer := range sources { for _, inner := range sources { if files.IsWithin(outer, inner) { return nil, invalid(errors.New("selection contains a folder and items inside it")) } } } return sources, nil } func checkTargets(sources []string, destination string) error { for _, source := range sources { name := filepath.Base(source) if _, err := os.Lstat(filepath.Join(destination, name)); err == nil { return fmt.Errorf("%w: %s", files.ErrDestinationExists, name) } } return nil } func (manager *Manager) moveFiles(job *Job, sources []string, destination string, cleanup bool) { job.resetProgress() var totalBytes int64 for _, source := range sources { if size, err := files.TreeSize(source); err == nil { totalBytes += size } } job.SetTotal(totalBytes) movedNames := make([]string, 0, len(sources)) defer func() { manager.recordHistory(job, destination, movedNames) }() started := time.Now() for _, source := range sources { if err := files.MoveInto(source, destination, manager.reservedBytes(), job.Add); err != nil { job.logger().Error("move failed", "item", filepath.Base(source), "destination", destination, "moved_before_failure", len(movedNames), "error", err, ) job.setStatus(StatusReady, "Move failed: "+explain.Error(err)) return } movedNames = append(movedNames, filepath.Base(source)) } job.logger().Info("move finished", "items", len(movedNames), "bytes", totalBytes, "destination", destination, "duration", time.Since(started).Round(time.Second).String(), ) message := fmt.Sprintf("Moved %d item(s) to %s", len(sources), destination) if !cleanup { job.setStatus(StatusReady, message) return } if err := os.RemoveAll(job.directory); err != nil { job.logger().Warn("could not remove temporary files after move", "directory", job.directory, "error", err) message += ", cleanup failed: " + explain.Error(err) } job.setStatus(StatusCompleted, message) }