Check rsync destination capacity before copying
This commit is contained in:
@@ -80,6 +80,7 @@ type RsyncOptions struct {
|
||||
PreserveXattrs bool `json:"preserveXattrs"`
|
||||
Checksum bool `json:"checksum"`
|
||||
DryRun bool `json:"dryRun"`
|
||||
SkipSpaceCheck bool `json:"skipSpaceCheck"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
|
||||
@@ -108,6 +108,68 @@ func (r *Runner) Run(ctx context.Context, runID string, job model.Job, callback
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// Estimate performs rsync's complete comparison without writing destination data.
|
||||
func (r *Runner) Estimate(ctx context.Context, runID string, job model.Job) (Summary, error) {
|
||||
job.Rsync.DryRun = true
|
||||
args := Arguments(job)
|
||||
for index, arg := range args {
|
||||
if strings.HasPrefix(arg, "--out-format=") {
|
||||
args[index] = "--out-format="
|
||||
}
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, r.Binary, args...)
|
||||
cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
cmd.Cancel = func() error {
|
||||
if cmd.Process == nil {
|
||||
return os.ErrProcessDone
|
||||
}
|
||||
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return Summary{}, err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return Summary{}, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
return Summary{}, fmt.Errorf("environment: rsync binary not found: %w", err)
|
||||
}
|
||||
return Summary{}, err
|
||||
}
|
||||
unregister := r.register(runID, cmd.Process)
|
||||
defer unregister()
|
||||
var summary Summary
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
scanOutput(stdout, func(line string) {
|
||||
if value, ok := parseCount(filesPattern, line); ok {
|
||||
summary.Files = value
|
||||
}
|
||||
if value, ok := parseCount(bytesPattern, line); ok {
|
||||
summary.Bytes = value
|
||||
}
|
||||
})
|
||||
close(done)
|
||||
}()
|
||||
errBytes, _ := io.ReadAll(io.LimitReader(stderr, 1024*1024))
|
||||
err = cmd.Wait()
|
||||
<-done
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return Summary{}, fmt.Errorf("cancelled: %w", ctx.Err())
|
||||
}
|
||||
return Summary{}, fmt.Errorf("rsync preflight: %s", strings.TrimSpace(string(errBytes)))
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func Arguments(job model.Job) []string {
|
||||
options := job.Rsync
|
||||
args := []string{"--recursive", "--human-readable", "--info=progress2", "--stats", "--partial", "--out-format=FILE|%n"}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.casaderoll.de/michael/urbm/internal/model"
|
||||
@@ -402,6 +403,23 @@ func (s *Service) executeRsync(ctx context.Context, run model.Run) (result model
|
||||
}
|
||||
return failed(run, "environment", "rsync target unavailable: "+err.Error())
|
||||
}
|
||||
if !job.Rsync.DryRun && !job.Rsync.SkipSpaceCheck {
|
||||
appendLiveLog(&live, "Rsync-Vorabprüfung ermittelt den tatsächlich zu übertragenden Datenumfang")
|
||||
s.updateActive(live)
|
||||
estimate, err := s.rsync.Estimate(ctx, run.ID, job)
|
||||
if err != nil {
|
||||
return failedError(run, err)
|
||||
}
|
||||
available, err := availableBytes(job.Rsync.Target)
|
||||
if err != nil {
|
||||
return failed(run, "environment", "read rsync target capacity: "+err.Error())
|
||||
}
|
||||
appendLiveLog(&live, fmt.Sprintf("Rsync-Vorabprüfung: %s zu übertragen, %s am Ziel frei", formatBytes(estimate.Bytes), formatBytes(available)))
|
||||
s.updateActive(live)
|
||||
if estimate.Bytes > available {
|
||||
return failed(run, "environment", fmt.Sprintf("Rsync-Ziel hat nicht genügend freien Speicher: ungefähr %s für %d Dateien erforderlich, aber nur %s frei unter %s", formatBytes(estimate.Bytes), estimate.Files, formatBytes(available), job.Rsync.Target))
|
||||
}
|
||||
}
|
||||
started := time.Now()
|
||||
lastLoggedPercent := -10
|
||||
summary, err := s.rsync.Run(ctx, run.ID, job, func(progress rsync.Progress) {
|
||||
@@ -442,6 +460,14 @@ func (s *Service) executeRsync(ctx context.Context, run model.Run) (result model
|
||||
return run
|
||||
}
|
||||
|
||||
func availableBytes(path string) (int64, error) {
|
||||
var stats syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stats); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(stats.Bavail) * int64(stats.Bsize), nil
|
||||
}
|
||||
|
||||
func (s *Service) executeBackup(ctx context.Context, run model.Run) (result model.Run) {
|
||||
job, ok := s.job(run.JobID)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user