145 lines
3.8 KiB
Go
Executable File
145 lines
3.8 KiB
Go
Executable File
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)
|
|
}
|