84 lines
2.3 KiB
Go
84 lines
2.3 KiB
Go
//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())
|
||
|
|
}
|
||
|
|
}
|