feat: persist resume metadata on SFTP single-file cancel
This commit is contained in:
@@ -725,6 +725,14 @@ func downloadSingleFile(ctx context.Context, c *client, req *core.DownloadReques
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Persist partial progress so the user can resume later
|
||||
// with --resume. Unconditional — mirrors the FTP and SFTP
|
||||
// parallel paths that always save a .goget.meta sidecar on
|
||||
// cancellation.
|
||||
if outputPath != "" && outputPath != "-" && total > 0 {
|
||||
meta := output.NewResumeMetadata(req.URL.String(), "", "", int64(total), totalSize)
|
||||
_ = meta.Save(outputPath)
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package sftp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
"codeberg.org/petrbalvin/goget/internal/output"
|
||||
)
|
||||
|
||||
// TestDownloadSingleFileGracefulShutdown verifies that downloadSingleFile
|
||||
// persists a .goget.meta sidecar when the context is cancelled mid-transfer.
|
||||
// Without this, a user hitting Ctrl-C during an SFTP single-file download
|
||||
// would lose all progress and be unable to --resume.
|
||||
func TestDownloadSingleFileGracefulShutdown(t *testing.T) {
|
||||
addr, cleanup := startTestServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Use a large file so the download takes multiple read iterations,
|
||||
// giving the context cancellation a chance to land between reads.
|
||||
bigContent := make([]byte, 1<<20) // 1 MiB
|
||||
if _, err := rand.Read(bigContent); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
origContent := testFileContent
|
||||
testFileContent = bigContent
|
||||
t.Cleanup(func() { testFileContent = origContent })
|
||||
|
||||
u, _ := url.Parse("sftp://user@" + addr + "/testfile")
|
||||
outDir := t.TempDir()
|
||||
outputPath := filepath.Join(outDir, "out")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Cancel the context on the first progress callback — at that point
|
||||
// at least one chunk has been written, so total > 0 and the
|
||||
// .goget.meta sidecar will be persisted.
|
||||
var once sync.Once
|
||||
progressCallback := func(current, total int64, speed float64) {
|
||||
once.Do(cancel)
|
||||
}
|
||||
|
||||
req := &core.DownloadRequest{
|
||||
URL: u,
|
||||
Output: outputPath,
|
||||
Ctx: ctx,
|
||||
SSHInsecure: true,
|
||||
ProgressCallback: progressCallback,
|
||||
}
|
||||
|
||||
_, err := download(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from cancelled context, got nil")
|
||||
}
|
||||
|
||||
// The output file must exist (partial data was written).
|
||||
if _, statErr := os.Stat(outputPath); statErr != nil {
|
||||
t.Fatalf("output file should exist: %v", statErr)
|
||||
}
|
||||
|
||||
// Verify .goget.meta was saved with partial progress.
|
||||
meta, err := output.LoadResumeMetadata(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadResumeMetadata: %v", err)
|
||||
}
|
||||
if meta == nil {
|
||||
t.Fatal("expected resume metadata after SIGINT, got nil")
|
||||
}
|
||||
if meta.Downloaded <= 0 {
|
||||
t.Errorf("Downloaded = %d, want > 0", meta.Downloaded)
|
||||
}
|
||||
if meta.URL != u.String() {
|
||||
t.Errorf("URL = %q, want %q", meta.URL, u.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user