9.3 KiB
Download Pipeline
This document explains how goget processes a download request from start to finish — the phase structure, parallel chunking, resume logic, and output writing.
High-Level Flow
flowchart TD
Start([User invokes goget])
ParseFlags["Parse CLI flags"]
LoadConfig["Load config + merge overrides"]
ResolveProtocol["Resolve protocol handler\nfrom URL scheme"]
CreateRequest["Build core.DownloadRequest"]
CheckResume{"Resume metadata\nexists?"}
LoadResume["Load .goget.meta\n(resume info + chunk map)"]
SetupOutput["Setup output writer\n(atomic temp file + resume)"]
CreateDirs{"--create-dirs\n(parent missing)?"}
MkdirAll["os.MkdirAll → create\nparent directories"]
CheckParallel{"File > 100 MB\nor --parallel set?"}
SequentialDownload["Sequential download\n(single connection)"]
ParallelDownload["Parallel chunked download\n(multiple Range requests)"]
Decompress{"Auto-decompress\nenabled?"}
Decompression["Decompress response\n(gzip, deflate, bzip2, zlib)"]
VerifyChecksum{"Checksum\nspecified?"}
ChecksumVerify["Verify checksum\n(SHA-256, SHA-512, etc.)"]
PGPVerify{"PGP verify/\ndecrypt?"}
PGPProcess["Verify signature /\ndecrypt file"]
SaveHSTS["Save HSTS cache\n(RFC 6797)"]
AtomicRename["Atomic rename\ntemp → final"]
SigInt{"SIGINT received?"}
SaveSidecar["Save .goget.meta\nfor resume"]
Exit130["Exit 130"]
RunHook{"--on-complete\nset?"}
PostHook["Run post-download command\n(GOGET_OUTPUT, GOGET_SIZE, GOGET_URL)"]
Done([Done])
Start --> ParseFlags --> LoadConfig --> ResolveProtocol
ResolveProtocol --> CreateRequest --> CheckResume
CheckResume -->|Yes| LoadResume --> SetupOutput
CheckResume -->|No| SetupOutput
SetupOutput --> CreateDirs
CreateDirs -->|Yes| MkdirAll --> CheckParallel
CreateDirs -->|No| CheckParallel
CheckParallel -->|Yes| ParallelDownload
CheckParallel -->|No| SequentialDownload
SequentialDownload --> Decompress
ParallelDownload --> Decompress
Decompress -->|Yes| Decompression --> VerifyChecksum
Decompress -->|No| VerifyChecksum
VerifyChecksum -->|Yes| ChecksumVerify -->|pass| PGPVerify
ChecksumVerify -->|fail| Done
VerifyChecksum -->|No| PGPVerify
PGPVerify -->|Yes| PGPProcess --> SaveHSTS
PGPVerify -->|No| SaveHSTS
SaveHSTS --> AtomicRename
AtomicRename --> SigInt
SigInt -->|Yes| SaveSidecar --> Exit130
SigInt -->|No| RunHook
RunHook -->|Yes| PostHook --> Done
RunHook -->|No| Done
Phase 1: Request Construction
A core.DownloadRequest is built from CLI flags and config:
req := &core.DownloadRequest{
URL: parsedURL,
Output: outputPath,
Resume: resumeEnabled,
Timeout: effectiveTimeout,
Verbose: verbose,
Headers: customHeaders,
Proxy: proxyURL,
Checksum: expectedChecksum,
Parallel: parallelConfig,
Recursive: recursiveEnabled,
MaxDepth: maxDepth,
ProgressCallback: progressFn,
Ctx: ctx,
}
Phase 2: Protocol Resolution
The protocol registry resolves the handler by URL scheme:
flowchart LR
URL["https://example.com"]
Parse["Parse scheme\n→ https"]
Normalized["Normalize\nhttps → http"]
Registry["Registry lookup\nprotocols[http]"]
Handler["HTTP protocol\nhandler"]
URL --> Parse --> Normalized --> Registry --> Handler
Scheme normalization maps:
https://→http(same handler)ftps://→ftp(same handler)webdavs://→webdav(same handler)
Phase 3: Output Setup
The internal/output package creates an atomic writer:
- Temp file — Writes to
<filename>.goget.tmpduring download - Parent directories — If
--create-dirsis enabled (default:true), missing parent directories are created viaos.MkdirAllbefore opening the output file. Pass--create-dirs=falseto restore strict behaviour - Resume metadata — Reads
<filename>.goget.metaif resuming - Progress callback — Hooks into the writer for real-time speed/ETA
writer, err := output.NewWriter(&output.WriterConfig{
Output: outputFile,
Atomic: true,
Resume: resumeEnabled,
CreateDirs: true, // auto-create parent dirs (curl --create-dirs)
ProgressCallback: progressFunc,
})
Resume Metadata Format
{
"downloaded_bytes": 524288000,
"etag": "\"abc123\"",
"last_modified": "Mon, 01 Jun 2026 12:00:00 GMT",
"url": "https://example.com/file.zip",
"last_write": "2026-06-01T12:05:00Z",
"chunks": {
"0": 131072000,
"1": 131072000,
"2": 131072000,
"3": 131072000
}
}
For parallel downloads, each chunk's progress is individually tracked.
Phase 4: Download Strategy
Sequential Download
Used when the file is under 100 MB or --parallel 1 is set:
resp, _ := client.Do(request)
io.Copy(writer, resp.Body)
Parallel Chunked Download
Triggered automatically for files over 100 MB, or explicitly with --parallel N:
sequenceDiagram
participant Main
participant Chunk1
participant Chunk2
participant Chunk3
participant Chunk4
participant Writer as Atomic Writer
Main->>Main: HEAD request → get file size
Main->>Main: Split into N equal chunks
Main->>Chunk1: Start: bytes 0-13107199
Main->>Chunk2: Start: bytes 13107200-26214399
Main->>Chunk3: Start: bytes 26214400-39321599
Main->>Chunk4: Start: bytes 39321600-52428799
Chunk1->>Writer: Write bytes to temp/chunk_0
Chunk2->>Writer: Write bytes to temp/chunk_1
Chunk3->>Writer: Write bytes to temp/chunk_2
Chunk4->>Writer: Write bytes to temp/chunk_3
Main->>Writer: Merge chunks → final file
Each chunk downloads via a separate HTTP Range request:
GET /large.iso HTTP/1.1
Host: example.com
Range: bytes=13107200-26214399
Concurrency Control
- Max 4 parallel connections by default
- Customizable via
--parallel Nor configparallelkey - Min chunk size: 1 MB
- Max chunk size: 50 MB
Phase 5: Post-Processing
Decompression
If auto_decompress is enabled (default) and the server sends compressed content, the response body is transparently decompressed:
| Content-Encoding | Handler |
|---|---|
gzip |
compress/gzip |
deflate |
compress/flate |
zlib |
Internal |
bzip2 |
Internal |
lzw |
Internal |
Use --no-decompress to preserve the compressed response.
Checksum Verification
If --checksum or --checksum-file is provided, the downloaded file is hashed and compared:
Expected: a1b2c3d4...
Actual: a1b2c3d4...
Checksum OK
Supported algorithms: SHA-256, SHA-512, SHA3-256, SHA3-512, BLAKE2b, MD5.
Atomic Rename
On successful completion, the temp file is atomically renamed:
os.Rename("file.zip.goget.tmp", "file.zip")
If the download fails or is interrupted, the temp file remains for resume.
PGP Verification and Decryption
If --pgp-verify or --pgp-decrypt is set, the downloaded file is processed via golang.org/x/crypto/openpgp:
- Detached signature verification —
--pgp-sig file.sig --pgp-key public.key - Decryption —
--pgp-decrypt --pgp-key private.key --pgp-passphrase "secret" - Auto-detect signature files:
.asc,.sig
The decrypted file replaces the encrypted one (.gpg → stripped extension, or .decrypted suffix).
HSTS Cache
After every successful HTTPS connection, the HSTS cache (~/.config/goget/hsts) is updated per RFC 6797. Expired entries are pruned on load. This ensures that future http:// requests to known hosts are automatically upgraded to https://.
Graceful Shutdown (SIGINT)
When the user sends Ctrl+C (SIGINT) during a download, goget persists partial progress to the .goget.meta sidecar for all protocols (HTTP, FTP, SFTP, WebDAV) and exits with code 130. A subsequent goget --resume picks up where it left off. The signal handler uses signal.NotifyContext and propagates cancellation through the download pipeline via ctx.Err() checks after every read.
Smart Timeout
When no explicit --timeout is set, goget calculates a timeout based on file size:
timeout = (fileSize / minSpeed) × safetyFactor
Where:
minSpeed= 10 KB/s (conservative minimum)safetyFactor= 3.0minTimeout= 30 secondsmaxTimeout= 24 hours
A 1 GB file at 10 KB/s → 34 hours (capped to 24 hours max). A 10 MB file at 10 KB/s → 51 minutes.
Rate Limiting
--rate-limit uses a token bucket algorithm:
bucket := transport.NewTokenBucket(rate)
// For each read:
bucket.Wait(n)
The token bucket allows short bursts above the limit while maintaining the average rate.
Speed Format
--rate-limit 1MB/s # 1,000,000 bytes/sec
--rate-limit 500KB/s # 500,000 bytes/sec
--rate-limit 1GB/s # 1,000,000,000 bytes/sec
--rate-limit 100 # 100 bytes/sec (plain number)
Retry Logic
When a download fails, goget retries with exponential backoff:
Attempt 1: wait 1s, retry
Attempt 2: wait 2s, retry
Attempt 3: wait 4s, retry
...
Attempt N: wait min(2^(N-1) × 1s, 30s), retry
Max retries default to 3. Use --max-retries N to increase. Use --retry-all-errors to retry on HTTP 4xx/5xx (by default, only 5xx server errors trigger retries).