rusty-cat 0.3.0

Async HTTP client for resumable file upload and download.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# rusty-cat

[![Crates.io](https://img.shields.io/crates/v/rusty-cat.svg)](https://crates.io/crates/rusty-cat)
[![Docs.rs](https://docs.rs/rusty-cat/badge.svg)](https://docs.rs/rusty-cat)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

`rusty-cat` is an async Rust SDK for resumable file upload and download. It gives applications a compact public facade for building transfer tasks, running those tasks in a background scheduler, receiving progress callbacks, and plugging in protocol-specific implementations such as plain HTTP, Aliyun OSS, Aliyun OSS presigned URLs, Azure Blob Storage, and Azure Blob SAS URLs.

The crate is designed for applications that need reliable large-file transfer without forcing a specific storage backend or database layer. The SDK handles scheduling, chunk dispatch, retry, pause/resume/cancel commands, and progress fan-out. Your application remains responsible for business records, credential management, user permissions, and provider-specific setup.

The recommended public import is:

```rust
use rusty_cat::api::*;
```

Existing module paths still work, but `rusty_cat::api::*` is the stable, beginner-friendly entry point. Using the facade also makes future refactoring easier because most application code can import SDK types from a single module.

## Package, platform, Rust, and license

| Item | Value |
|---|---|
| Crate | `rusty-cat` |
| Version | `0.2.4` |
| Rust edition | 2021 |
| Runtime | Tokio-based async runtime hosted by an internal scheduler thread |
| HTTP stack | `reqwest` with `rustls-tls` |
| Platforms | Linux, macOS, and Windows targets supported by stable Rust, Tokio, and reqwest |
| License | MIT |
| Repository | <https://github.com/0barman/rusty-cat> |

### Badge Markdown

The Crates.io, Docs.rs, and License badge Markdown is shown below. These badges are safe to paste into downstream README files or generated documentation pages:

```markdown
[![Crates.io](https://img.shields.io/crates/v/rusty-cat.svg)](https://crates.io/crates/rusty-cat)
[![Docs.rs](https://docs.rs/rusty-cat/badge.svg)](https://docs.rs/rusty-cat)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
```

## Feature highlights

### Capability matrix

| Capability | Supported | Notes |
|---|:---:|---|
| HTTP resumable upload | Yes | Upload tasks are split into chunks and delegated to a `BreakpointUpload` implementation. The default style supports multipart/form-data chunk requests, and provider plugins can replace the request logic. |
| HTTP resumable download | Yes | Download tasks use `HEAD` during preparation and `GET` with `Range` headers for chunk transfer through `StandardRangeDownload`. |
| In-memory upload source | Yes | `UploadPounceBuilder::from_bytes(file_name, bytes, chunk_size)` uploads from an in-memory buffer instead of a file path; chunking and sizing behave the same as file-backed uploads. |
| Aliyun OSS direct upload/download | Yes | Enable `aliyun-oss-direct`; use `AliOssDirectUpload` and `AliOssDirectDownload` when the client process is trusted to hold AccessKey credentials. |
| Aliyun OSS presigned upload/download | Yes | Enable `aliyun-oss-presigned`; use short-lived presigned part and range URLs generated by your backend. |
| Azure Blob direct upload/download | Yes | Enable `azure-blob-direct`; use Shared Key-authenticated block upload and range download when the client process is trusted to hold the storage account key. |
| Azure Blob SAS upload/download | Yes | Enable `azure-blob-sas`; use short-lived SAS URLs generated by your backend. |
| Provider-neutral presigned primitives | Yes | Enable `presigned` to use `PresignedMultipartUpload`/`PresignedRangeDownload` and their plans against any S3/OSS-style backend your server can presign, without a provider-specific feature. |
| Presigned plan validation | Yes | `PresignedMultipartUploadPlan::validate()` rejects zero-size parts, duplicate offsets, and parts outside the declared object size before any byte is sent. |
| Presigned completion/abort callbacks | Yes | A plan can carry `complete_request`/`abort_request` plus an optional `PresignedCompletionBodyBuilder`, so your backend can verify and merge parts (or clean up) when a transfer ends. |
| Presigned URL refresh on expiry | Yes | `PresignedUploadUrlRefresher`/`PresignedDownloadUrlRefresher` with `refresh_before_secs` refresh part/range URLs before they expire during long transfers. |
| Upload concurrency setting | Yes | `MeowConfig::builder().max_upload_concurrency(n)` limits the number of upload groups running at the same time. |
| Download concurrency setting | Yes | `MeowConfig::builder().max_download_concurrency(n)` limits the number of download groups running at the same time. |
| Upload progress | Yes | Per-task progress callbacks passed to `MeowClient::try_enqueue(...)` receive `FileTransferRecord` snapshots. |
| Download progress | Yes | The same callback model is used for downloads, so upload and download UI code can share one progress-record handler. |
| Global progress listener | Yes | `register_global_progress_listener(...)` observes all tasks created by the client, which is useful for dashboards and persistence workers. |
| Global SDK debug logs | Yes | `set_debug_log_listener(...)` installs a process-global SDK log listener for diagnostics and integration tests. |
| Application-managed persistence | Yes | The SDK intentionally does not persist transfer state in an embedded database, so it can fit server, desktop, mobile, and CLI applications. |
| Custom database adaptation | Yes | Persist records from callbacks/listeners in your own database and rebuild tasks after restart. |
| Callback panic isolation | Yes | User callbacks are isolated from scheduler execution; callbacks should still be fast, non-blocking, and panic-free. |
| Chunk failure retry | Yes | `with_max_chunk_retries(...)` on upload and download builders controls additional retries after the first failed chunk transfer. |
| Upload prepare retry | Yes | `UploadPounceBuilder::with_max_upload_prepare_retries(...)` controls additional retries after the first failed upload preparation attempt. |
| Transport-aware retry & backoff | Yes | Beyond the retry counts above, transient transport failures (connection reset, timeout, incomplete message) are retried with exponential backoff and jitter, while non-transient errors fail fast. |
| Disk-full & local-file-removed detection | Yes | Local I/O failures are classified into dedicated error codes `DiskFull` and `LocalFileRemoved`, so callers can react specifically instead of treating every I/O error the same. |
| Pause/resume/cancel | Yes | Use `pause(...)`, `resume(...)`, and `cancel(...)` with the returned `TaskId`. |
| Paused import / selective restore | Yes | `try_enqueue_paused(...)` imports a task in the paused state with no network/file I/O; resume only the user-selected subset on restart. |
| Resume after process restart / crash | Yes | Rebuild the task and re-enqueue after a restart: downloads resume from the on-disk partial file, default uploads from the server `nextByte`, and presigned uploads from re-injected parts. See [Resuming after a restart]docs/resume-after-restart.md. |
| Presigned multipart resume across restart | Yes | `PresignedMultipartUpload::with_resumed_parts(...)` re-injects parts persisted by a previous run; `prepare` resumes past the longest verified contiguous prefix and re-sends the rest. |
| Provider multipart session id surfacing | Yes | `UploadResumeInfo::provider_upload_id` and `AliOssDirectUpload::current_upload_id()` expose the provider `UploadId` (not a secret) so orphaned multipart sessions can be aborted out of band. |
| Upload abort on cancel | Yes | Canceling an upload runs the protocol's `abort_upload` (for example OSS `AbortMultipartUpload`) so uncommitted parts/blocks stop accruing storage cost. |
| Snapshot diagnostics | Yes | `snapshot()` returns queued and active scheduler state for monitoring and troubleshooting. |
| Custom HTTP client | Yes | Inject a preconfigured `reqwest::Client` with `MeowConfigBuilder::http_client(...)` for proxy, TLS, default headers, or observability integration. |
| Custom upload protocol | Yes | Implement `BreakpointUpload` to integrate business-specific upload APIs. |
| Custom download protocol | Yes | Implement `BreakpointDownload` to integrate custom range-download authentication or headers. |
| Custom upload request method/headers | Yes | `with_method(...)` and `with_headers(...)` on `UploadPounceBuilder` customize the default upload request line and headers. |
| Per-task download HTTP override | Yes | `DownloadPounceBuilder::with_breakpoint_download_http(...)` overrides the range `Accept` header for a single task without changing global config. |

### Architecture overview

| Layer | Main types | Responsibility |
|---|---|---|
| Public facade | `rusty_cat::api::*` | One import point for client, config, task builders, callbacks, errors, status, logs, and optional providers. |
| Client | `MeowClient` | Owns immutable config, lazily starts the executor, submits tasks, controls lifecycle, and manages listeners. |
| Config | `MeowConfig`, `MeowConfigBuilder` | Defines concurrency, queue capacities, HTTP timeout/keepalive, range-download behavior, and optional custom HTTP client. |
| Task builders | `UploadPounceBuilder`, `DownloadPounceBuilder` | Convert simple parameters into executable `PounceTask` values. |
| Scheduler | Internal executor | Runs background workers, queues tasks, dispatches chunks, retries failures, and emits events. |
| Protocol plugins | `BreakpointUpload`, `BreakpointDownload` | Implement provider-specific signing, presigned URLs, chunk requests, and completion behavior. |
| Observability | `FileTransferRecord`, `TransferSnapshot`, `Log` | Per-task progress, global progress events, queue snapshots, and debug logs. |

## Quick start

Add the crate:

```toml
[dependencies]
rusty-cat = "0.2.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

For OSS providers, enable only what you need. Keeping the feature list small reduces optional dependencies and makes it clearer which cloud integrations your application actually uses:

```toml
[dependencies]
rusty-cat = { version = "0.2.4", features = ["aliyun-oss-direct"] }
```

| Feature | Purpose |
|---|---|
| `aliyun-oss-direct` | Aliyun OSS direct upload/download with AccessKey credentials and OSS Signature Version 4 signing. |
| `aliyun-oss-presigned` | Aliyun OSS presigned multipart upload and range download helpers. |
| `azure-blob-direct` | Azure Blob upload/download with Shared Key authentication. |
| `azure-blob-sas` | Azure Blob SAS upload/download helpers. |
| `presigned` | Provider-neutral presigned multipart/range primitives. |
| `aliyun-oss` | Convenience umbrella that currently enables `aliyun-oss-presigned` only. It does **not** pull in `aliyun-oss-direct`. |
| `azure-blob` | Convenience umbrella that currently enables `azure-blob-sas` only. It does **not** pull in `azure-blob-direct`. |
| `oss-providers` | Both umbrellas together: `aliyun-oss` + `azure-blob` (Aliyun OSS presigned + Azure Blob SAS). It does not enable the direct/AccessKey/Shared Key features. |
| `all` | Enables all four provider features (`aliyun-oss-direct`, `aliyun-oss-presigned`, `azure-blob-direct`, `azure-blob-sas`). Use it for examples, not minimal production builds. |

The `aliyun-oss`, `azure-blob`, and `oss-providers` umbrellas intentionally select the **presigned/SAS** flows, because those are the recommended model for untrusted clients (your backend holds the credentials). When you need the direct AccessKey/Shared Key flows, enable `aliyun-oss-direct` and/or `azure-blob-direct` explicitly.

For a focused comparison of direct credentials versus presigned/SAS URLs, see [Provider feature flags: direct vs presigned/SAS](docs/provider-feature-flags.md).

### Complete end-to-end example

This example starts from `MeowConfig`, creates a `MeowClient`, registers listeners, builds a task, submits it, waits for the completion/failure signal, inspects a snapshot, and closes the client. It uses an HTTP range download task because that path works without cloud credentials; the same client lifecycle applies to upload tasks and OSS/Azure provider tasks.

```rust,no_run
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;

use rusty_cat::api::{
	DownloadPounceBuilder, FileTransferRecord, Log, MeowClient, MeowConfig, TransferStatus,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
	let config = MeowConfig::builder()
		.max_upload_concurrency(2)
		.max_download_concurrency(2)
		.http_timeout(Duration::from_secs(30))
		.tcp_keepalive(Duration::from_secs(60))
		.command_queue_capacity(256)
		.worker_event_queue_capacity(1024)
		.build()?;

	let client = MeowClient::new(config);

	client.set_debug_log_listener(Some(Arc::new(|log: Log| {
		println!("[rusty-cat] {log}");
	})))?;

	let global_listener_id = client.register_global_progress_listener(|record| {
		println!(
			"global: task={} file={} progress={:.2}% status={:?}",
			record.task_id(),
			record.file_name(),
			record.progress() * 100.0,
			record.status(),
		);
	})?;

	let task = DownloadPounceBuilder::new(
		"example.bin",
		"./downloads/example.bin",
		1024 * 1024,
		"https://example.com/example.bin",
	)
	.with_client_file_sign("business-file-id-001")
	.with_max_chunk_retries(3)
	.build();

	let (tx, rx) = mpsc::channel::<Result<(), String>>();
	let tx = Arc::new(Mutex::new(Some(tx)));
	let progress_tx = Arc::clone(&tx);
	let complete_tx = Arc::clone(&tx);

	let task_id = client
		.try_enqueue(
			task,
			move |record: FileTransferRecord| {
				println!(
					"task={} progress={:.2}% status={:?}",
					record.task_id(),
					record.progress() * 100.0,
					record.status(),
				);

				match record.status() {
					TransferStatus::Failed(err) => {
						if let Ok(mut guard) = progress_tx.lock() {
							if let Some(tx) = guard.take() {
								let _ = tx.send(Err(format!("transfer failed: {err}")));
							}
						}
					}
					TransferStatus::Canceled => {
						if let Ok(mut guard) = progress_tx.lock() {
							if let Some(tx) = guard.take() {
								let _ = tx.send(Err("transfer canceled".to_string()));
							}
						}
					}
					_ => {}
				}
			},
			move |task_id, provider_payload| {
				println!("task {task_id} complete; payload={provider_payload:?}");
				if let Ok(mut guard) = complete_tx.lock() {
					if let Some(tx) = guard.take() {
						let _ = tx.send(Ok(()));
					}
				}
			},
		)
		.await?;

	println!("enqueued task: {task_id}");

	match rx.recv_timeout(Duration::from_secs(300)) {
		Ok(Ok(())) => println!("transfer finished"),
		Ok(Err(err)) => eprintln!("transfer ended with error: {err}"),
		Err(err) => eprintln!("timed out while waiting for completion callback: {err}"),
	}

	let snapshot = client.snapshot().await?;
	println!(
		"snapshot: queued={}, active={}",
		snapshot.queued_groups, snapshot.active_groups
	);

	client.unregister_global_progress_listener(global_listener_id)?;
	client.clear_global_listener()?;
	client.set_debug_log_listener(None)?;
	client.close().await?;
	assert!(client.is_closed());
	Ok(())
}
```

### `MeowClient` API guide

| Function | Use it when | Important notes |
|---|---|---|
| `MeowClient::new(config)` | Create the SDK entry point. | The executor starts lazily on the first task operation. `MeowClient` is not `Clone` because it owns scheduler state; wrap it in `Arc<MeowClient>` when multiple async tasks or threads need shared access. |
| `http_client()` | Need a `reqwest::Client` aligned with SDK config. | Returns the injected custom client when one was configured; otherwise builds a client from `http_timeout` and `tcp_keepalive`. This is useful when protocol code outside the executor must make compatible HTTP calls. |
| `register_global_progress_listener(listener)` | Observe all task progress records. | Returns a `GlobalProgressListenerId`. Use this for UI-wide progress aggregation, persistence queues, or monitoring. Keep callback work fast. |
| `unregister_global_progress_listener(id)` | Remove one global listener. | Returns `Ok(false)` when the ID does not exist, so cleanup code can call it safely. |
| `clear_global_listener()` | Remove every global progress listener. | Useful during shutdown, integration-test cleanup, or application logout flows. |
| `set_debug_log_listener(Some(listener))` | Receive SDK debug logs. | The listener is process-global rather than client-local. Pass `None` to clear it before shutdown or when tests need isolation. |
| `try_enqueue(task, progress_cb, complete_cb).await` | Submit an upload/download task. | This performs asynchronous submission, not synchronous transfer completion. It fails fast when the command queue is full. Store the returned `TaskId` for pause/resume/cancel operations. |
| `try_enqueue_paused(task, progress_cb, complete_cb).await` | Import a task in the paused state without scheduling it. | Performs no network or file I/O until you call `resume(...)`. Use it for restart/restore: import many tasks, then resume only the user-selected subset. Same fail-fast back-pressure as `try_enqueue`. See the persistence section below. |
| `pause(task_id).await` | Pause a queued or running task. | Sends a command to the scheduler. A paused task can be resumed later with the same `TaskId`. |
| `resume(task_id).await` | Continue a paused task. | Keeps the same `TaskId` and asks the scheduler to continue from available local/remote progress. |
| `cancel(task_id).await` | Stop a task. | Cancellation is best-effort and may run provider cleanup such as aborting a multipart session. Treat canceled tasks as terminal unless your application deliberately creates a new task. |
| `snapshot().await` | Inspect queued and active groups. | Useful for dashboards, health checks, and debugging scheduler behavior under concurrency. |
| `close().await` | Shut down. | Mandatory for clean shutdown: cancels in-flight work, flushes `Paused` events, drains callbacks, and joins the scheduler thread. Do not rely on `Drop` for production shutdown. |
| `is_closed()` | Check whether the client is closed. | A successfully closed client cannot be reopened; create a new `MeowClient` if you need to submit more work. |

There is no public `enqueue(...)` method in the current API. Use `try_enqueue(...)`; the name is intentional because enqueue uses fail-fast backpressure. If your application submits many tasks at once, increase `command_queue_capacity` or retry `CommandSendFailed` with your own backoff policy.

## Configuration parameters

### `MeowConfig` and `MeowConfigBuilder`

Start with `MeowConfig::default()` for a safe baseline or use `MeowConfig::builder()` for validated customization. The configuration is immutable after the client is created, which prevents accidental runtime changes from affecting tasks already in the scheduler.

| Parameter | Default | Constraint | Description |
|---|---:|---|---|
| `max_upload_concurrency` | `2` | `>= 1` | Maximum upload groups processed concurrently. |
| `max_download_concurrency` | `2` | `>= 1` | Maximum download groups processed concurrently. |
| `breakpoint_download_http.range_accept` | `application/octet-stream` | Valid header value | Default `Accept` header for range download chunks. |
| `http_client` | `None` | Reusable `reqwest::Client` | Optional custom HTTP client for proxy, TLS, default headers, or observability. |
| `http_timeout` | `5s` | Positive duration | Per-request timeout for internally built HTTP clients. |
| `tcp_keepalive` | `30s` | Positive duration | TCP keepalive for internally built HTTP clients. |
| `command_queue_capacity` | `128` | `>= 1` | Queue for enqueue, pause, resume, cancel, snapshot, and close commands. |
| `worker_event_queue_capacity` | `256` | `>= 1` | Queue for progress/state events. |

| Builder/accessor | Description |
|---|---|
| `MeowConfig::builder()` | Creates a builder initialized with defaults. |
| `max_upload_concurrency(n)` / `max_upload_concurrency()` | Sets/reads upload concurrency. Recommended range: `1..=64`. |
| `max_download_concurrency(n)` / `max_download_concurrency()` | Sets/reads download concurrency. Recommended range: `1..=64`. |
| `http_client(client)` | Injects a custom `reqwest::Client` for proxy, TLS, headers, or observability. |
| `http_timeout(duration)` / `http_timeout()` | Sets/reads HTTP timeout. Typical range: `3s..=60s`. |
| `tcp_keepalive(duration)` / `tcp_keepalive()` | Sets/reads TCP keepalive. Typical range: `15s..=120s`. |
| `command_queue_capacity(n)` / `command_queue_capacity()` | Sets/reads control queue capacity. |
| `worker_event_queue_capacity(n)` / `worker_event_queue_capacity()` | Sets/reads worker event queue capacity. |
| `breakpoint_download_http(config)` / `breakpoint_download_http()` | Sets/reads range-download HTTP behavior. |
| `build()` | Validates constraints and returns `MeowConfig`. |

### `UploadPounceBuilder`

| Method | Required? | Description |
|---|:---:|---|
| `UploadPounceBuilder::new(file_name, file_path, chunk_size)` | Yes | Creates a file-backed upload task. `chunk_size == 0` is normalized to the SDK default. |
| `UploadPounceBuilder::from_bytes(file_name, bytes, chunk_size)` | Alternative | Creates an in-memory upload task. The `Vec<u8>` is moved into `bytes::Bytes`. |
| `with_url(url)` | Usually yes | Sets target upload URL. For direct OSS/Azure, this is the final object/blob URL. For presigned flows, it is commonly the first part URL or logical target URL. |
| `with_file_path(path)` | Optional | Replaces the local file source. |
| `with_bytes(bytes)` | Optional | Replaces the source with in-memory bytes. |
| `with_method(method)` | Optional | Sets HTTP method for default/custom upload requests. Default is `POST`. |
| `with_headers(headers)` | Optional | Replaces base request headers. |
| `with_breakpoint_upload(upload)` | Optional | Sets a per-task custom `BreakpointUpload`, such as Aliyun/Azure direct or presigned upload. |
| `with_max_chunk_retries(retries)` | Optional | Sets additional retries after the first failed chunk attempt. `0` disables chunk retry. Default is `3`. |
| `with_max_upload_prepare_retries(retries)` | Optional | Sets additional retries after the first failed upload prepare attempt. Default is `3`. |
| `build()` | Yes | Reads file metadata for file-backed uploads and returns `PounceTask`; may return `std::io::Error`. |

Beginner tips:

- Use a `chunk_size` between `1 MiB` and `8 MiB` for common object storage workloads unless your provider requires a different size. Very small chunks increase request overhead; very large chunks reduce retry granularity.
- Put provider protocol objects in `Arc` and pass them to `with_breakpoint_upload(...)` because the executor can move transfer work across async tasks.
- For restart recovery, persist enough business metadata in your own database to rebuild the same logical task later, including local path, remote URL/object key, direction, chunk size, and provider type.

When you do not attach a provider plugin, uploads use the built-in default protocol. Its exact request/response format — and how the `fileMd5` signature is derived — is documented in [Default HTTP upload protocol contract](docs/default-http-upload-protocol.md).

### `DownloadPounceBuilder`

| Method | Required? | Description |
|---|:---:|---|
| `DownloadPounceBuilder::new(file_name, file_path, chunk_size, url)` | Yes | Creates a range-download task. The SDK uses `HEAD` for prepare and `GET` with `Range` for chunks. |
| `with_url(url)` | Optional | Replaces the remote download URL. |
| `with_file_path(path)` | Optional | Replaces the local output path. |
| `with_headers(headers)` | Optional | Replaces base request headers for `HEAD` and range `GET`. |
| `with_client_file_sign(sign)` | Optional | Sets a client-defined file signature shown in progress records. Useful for database keys. |
| `with_breakpoint_download(download)` | Optional | Sets a per-task custom `BreakpointDownload`, such as Aliyun/Azure direct or presigned range download. |
| `with_breakpoint_download_http(config)` | Optional | Overrides per-task range download HTTP behavior. |
| `with_max_chunk_retries(retries)` | Optional | Sets additional retries after the first failed range chunk attempt. `0` disables chunk retry. Default is `3`. |
| `build()` | Yes | Returns `PounceTask`. Validation happens during enqueue/runtime. |

Download HTTP methods are intentionally not configurable. Resumable HTTP download depends on standard `HEAD` and `GET` range behavior. If a gateway or provider needs a non-standard method, implement `BreakpointDownload` and inject it with `with_breakpoint_download(...)`.

## Error handling and retries

Most SDK calls return `Result<_, MeowError>`. `MeowError::code()` is a stable numeric code (an `i32`, suitable for FFI or structured logging) and `MeowError::msg()` is a human-readable message. Branch on `code()`, never on the message text. The numeric codes come from `InnerErrorCode`.

The codes you will most often branch on:

| Code | `InnerErrorCode` | When it happens |
|---:|---|---|
| `102` | `ParameterEmpty` | A required value (URL, file name, non-zero size) was empty at enqueue. |
| `103` | `DuplicateTaskError` | The same file/task is already queued, running, or paused. |
| `107` | `ClientClosed` | The client was closed; create a new `MeowClient` to submit more work. |
| `108` | `TaskNotFound` | `pause`/`resume`/`cancel` referenced an unknown or already-terminal `TaskId`. |
| `111` | `CommandSendFailed` | The command queue is full (fail-fast back-pressure). Retry with backoff or raise `command_queue_capacity`. |
| `117` | `InvalidTaskState` | The operation is invalid in the task's current state (for example resuming a task that is not paused). |
| `120` | `TaskCanceled` | The task was canceled before reaching `Complete`. |
| `121` | `DiskFull` | The local disk ran out of space while writing a download. |
| `122` | `LocalFileRemoved` | The local source/target file was removed or replaced mid-transfer. |

See the [Error codes reference](docs/error-codes.md) for the complete list (codes `101`–`122`), with the meaning and suggested handling of every variant.

### Retry and transient errors

Two builder knobs control how many times a failed step is retried before the task fails:

- `with_max_chunk_retries(n)` — extra attempts after the first failed chunk transfer (default `3`; `0` disables chunk retry).
- `UploadPounceBuilder::with_max_upload_prepare_retries(n)` — extra attempts after the first failed upload prepare (default `3`).

Within those budgets the SDK only retries **transient transport failures** (connection reset, timeout, incomplete message), and it waits between attempts using **exponential backoff with jitter**, so a flaky network or a briefly overloaded server does not trigger a retry storm. Non-transient errors (an HTTP `403`, a malformed response, an invalid range) fail fast without consuming the retry budget, because retrying them would not help.

To continue a task that ultimately failed (or that you paused), rebuild it and call `try_enqueue` again, or `resume(...)` a paused one; both continue from the last checkpoint. See [Resuming uploads and downloads after a restart](docs/resume-after-restart.md).

## OSS upload/download developer guides

OSS and Blob workflows are provider-specific, so detailed beginner guides live in separate documents. The SDK does not persist your keys, secrets, account keys, tokens, presigned URLs, or SAS URLs in a built-in database or credential store. Some values are held in memory while executing tasks. You must provide them from your application or trusted backend, and you should avoid logging them in progress callbacks or debug listeners.

If you are deciding which provider feature to enable first, start with [Provider feature flags: direct vs presigned/SAS](docs/provider-feature-flags.md).

| Guide | Feature flag | Example source |
|---|---|---|
| [Aliyun OSS direct upload/download]docs/aliyun-oss-direct.md | `aliyun-oss-direct` | [examples/aliyun_oss_direct_chunk_transfer.rs]examples/aliyun_oss_direct_chunk_transfer.rs |
| [Aliyun OSS presigned upload/download]docs/aliyun-oss-presigned.md | `aliyun-oss-presigned` | [examples/aliyun_oss_presigned_chunk_transfer.rs]examples/aliyun_oss_presigned_chunk_transfer.rs |
| [Azure Blob direct upload/download]docs/azure-blob-direct.md | `azure-blob-direct` | [examples/azure_blob_direct_chunk_transfer.rs]examples/azure_blob_direct_chunk_transfer.rs |
| [Azure Blob SAS upload/download]docs/azure-blob-sas.md | `azure-blob-sas` | [examples/azure_blob_sas_chunk_transfer.rs]examples/azure_blob_sas_chunk_transfer.rs |

## Persistence and custom database integration

`rusty-cat` intentionally has no built-in database. This keeps the SDK small and lets you choose SQLite, PostgreSQL, Redis, a mobile database, or an existing business persistence layer. The SDK emits progress records and terminal states; your application decides how those records map to durable business state.

Recommended pattern:

1. Create your own transfer table with fields such as business file ID, local path, remote URL/object key, direction, chunk size, provider, status, progress, and credential reference.
2. Register per-task and/or global progress callbacks.
3. In callbacks, persist `FileTransferRecord` values or forward them to a persistence worker. Do not perform slow database writes directly on the callback path; prefer batching or sending records to your own worker queue.
4. On process restart, query unfinished rows and rebuild equivalent `PounceTask` values.
5. Call `try_enqueue(...)` again. Provider protocols can resume from local/remote checkpoint information when the same logical task is recreated correctly.

Never persist raw cloud secrets unless your security model explicitly allows it. Prefer storing a reference to a backend-owned credential or generating fresh short-lived presigned/SAS URLs.

> **New to restart recovery?** For a step-by-step, beginner-friendly walkthrough of resuming after the process is killed or crashes — covering exactly what to persist for downloads, default HTTP uploads, and presigned multipart uploads, with copy-pasteable code — see [Resuming uploads and downloads after a process restart or crash]docs/resume-after-restart.md.

### Importing tasks in the paused state (selective restore)

`try_enqueue_paused(task, progress_cb, complete_cb)` imports a task in the `Paused` state **without scheduling it**. Unlike `try_enqueue`, it performs no network or file I/O: the task is registered into the scheduler and a single `Paused` progress record is emitted, but no `HEAD`/`GET`/upload request is sent and no file is opened until you start it.

This is the entry point for "restore on restart, then let the user choose what to download now":

1. On restart, rebuild a `PounceTask` for each unfinished row in your database.
2. Import each one with `try_enqueue_paused(...)` and keep the returned `TaskId`.
3. Render your task list from your own persisted progress. The `Paused` record reports `0.0` progress because no `prepare` has run yet, so the SDK does not know the real offset until the task is resumed.
4. When the user selects which transfers to start, call `resume(task_id)` for those; the rest stay paused. Downloads resume from the on-disk partial file length; uploads resume from the server-reported `next_byte`.

```rust,no_run
use rusty_cat::api::{DownloadPounceBuilder, MeowClient, MeowConfig};

async fn restore_and_start(client: &MeowClient) -> Result<(), rusty_cat::api::MeowError> {
    // Rebuilt from your own database after a restart.
    let task = DownloadPounceBuilder::new(
        "report.bin",
        "./downloads/report.bin",
        1024 * 1024,
        "https://example.com/report.bin",
    )
    .build();

    // Imported paused: no HTTP request is sent and no file is opened here.
    let task_id = client
        .try_enqueue_paused(task, |_record| {}, |_id, _payload| {})
        .await?;

    // Later, when the user chooses to start this transfer:
    client.resume(task_id).await?;
    Ok(())
}
```

See [examples/restore_import_paused.rs](examples/restore_import_paused.rs) for a complete, runnable demonstration that imports several tasks paused and resumes only a selected subset.

## Examples

| Example | What it demonstrates |
|---|---|
| [examples/http_local_chunk_transfer.rs]examples/http_local_chunk_transfer.rs | Local plain HTTP range download and custom binary upload protocol. |
| [examples/restore_import_paused.rs]examples/restore_import_paused.rs | Paused import + selective resume (restart/restore) against a local range server. |
| [examples/resume_after_restart.rs]examples/resume_after_restart.rs | Resume a download from a partial file left by a previous (killed) run. |
| [examples/aliyun_oss_direct_chunk_transfer.rs]examples/aliyun_oss_direct_chunk_transfer.rs | Aliyun OSS direct upload/download with AccessKey signing. |
| [examples/aliyun_oss_direct_custom_chunk_transfer.rs]examples/aliyun_oss_direct_custom_chunk_transfer.rs | Aliyun OSS direct custom chunk transfer. |
| [examples/aliyun_oss_presigned_chunk_transfer.rs]examples/aliyun_oss_presigned_chunk_transfer.rs | Aliyun OSS presigned multipart upload and range download. |
| [examples/azure_blob_direct_chunk_transfer.rs]examples/azure_blob_direct_chunk_transfer.rs | Azure Blob direct upload/download with Shared Key. |
| [examples/azure_blob_direct_custom_chunk_transfer.rs]examples/azure_blob_direct_custom_chunk_transfer.rs | Azure Blob direct custom chunk transfer. |
| [examples/azure_blob_sas_chunk_transfer.rs]examples/azure_blob_sas_chunk_transfer.rs | Azure Blob upload/download with SAS URLs. |

## Shutdown checklist

- Keep callbacks short and non-blocking.
- Store every returned `TaskId` if you plan to pause, resume, cancel, or inspect a task.
- Use `snapshot()` for runtime diagnostics.
- Always call `close().await` during shutdown.
- Recreate a new `MeowClient` after a successful `close()` if you need to submit more work.