Skip to content

Commit 2f9dd3b

Browse files
authored
Merge pull request #29 from ghimiresdp/feature/max-chunk-size
Allow passing custom chunk size to download files through config
2 parents 44f50c3 + 24e2d16 commit 2f9dd3b

5 files changed

Lines changed: 78 additions & 25 deletions

File tree

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ furl is a high-performance command-line tool designed to download files faster
99
by utilizing multiple threads to fetch chunks of data concurrently. Inspired by
1010
the simplicity of cURL and the robustness of wget.
1111

12+
> [!Warning]
13+
> This branch is usually ahead of the latest release. If something does not
14+
> work as expected, consider checking out a specific release tag, or install
15+
> a stable version via cargo or winget. For more, see the [Installation](#installation) section.
16+
1217
![Example image](res/images/example.png)
1318

1419
## ✨ Features
@@ -122,8 +127,8 @@ async fn main(){
122127
}
123128
```
124129

125-
For a runnable workspace example that embeds `furl-cli` as a library,
126-
check [examples/embedded-minimal](examples/embedded-minimal).
130+
For runnable workspace examples that embed `furl-cli` as a library,
131+
check the [examples/](examples) directory (e.g. [embedded-minimal](examples/embedded-minimal), [no-indicator](examples/no-indicator)).
127132

128133
## 🏗 Architecture Decisions
129134

@@ -145,14 +150,14 @@ directory. These are standalone workspace crates that show how to use
145150
- [x] Real-time progress bars
146151
- [x] Smart Threading (Completely ignore threading for files smaller than 1 MB).
147152
- [x] Package manager support (Windows: WinGet)
153+
- [x] Examples
154+
- [ ] Config file support (furl.toml)
155+
- [ ] Support for Proxy and Basic Auth
156+
- [ ] Resume interrupted downloads (Checkpoints)
148157
- [ ] Package manager support (Linux: APT)
149158
- [ ] Package manager support (Linux: Flatpak)
150159
- [ ] Package manager support (Linux: Snap)
151160
- [ ] Package manager support (macOS: Homebrew)
152-
- [ ] Examples
153-
- [ ] Resume interrupted downloads (Checkpoints)
154-
- [ ] Support for Proxy and Basic Auth
155-
- [ ] Config file support (furl.toml)
156161

157162
## 🤝 Contributing
158163

crates/furl_cli/src/engine.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,37 @@ impl Chunk {
5555
}
5656
}
5757

58+
#[derive(Clone)]
59+
pub struct DownloadConfig {
60+
pub max_chunk_size: u64,
61+
}
62+
63+
impl DownloadConfig {
64+
pub fn new() -> Self {
65+
Self {
66+
max_chunk_size: _10MB,
67+
}
68+
}
69+
pub fn set_max_chunk_size(mut self, size: u64) -> Self {
70+
self.max_chunk_size = size;
71+
self
72+
}
73+
}
74+
75+
impl Default for DownloadConfig {
76+
fn default() -> Self {
77+
Self::new()
78+
}
79+
}
80+
5881
pub struct Downloader {
5982
url: String,
6083
headers: HeaderMap,
6184
file_size: Option<u64>,
6285
filename: Option<String>,
6386
chunks: Arc<Mutex<Vec<Chunk>>>, // this stores downloaded chunk size
6487
reporter: Arc<dyn ProgressReporter + Send + Sync>,
88+
config: Arc<DownloadConfig>,
6589
}
6690

6791
pub trait HeaderUtils {
@@ -141,9 +165,15 @@ impl Downloader {
141165
filename: None,
142166
chunks: Arc::new(Mutex::new(Vec::new())),
143167
reporter: Arc::new(NoopReporter),
168+
config: Arc::new(DownloadConfig::default()),
144169
}
145170
}
146171

172+
pub fn with_config(mut self, config: DownloadConfig) -> Self {
173+
self.config = Arc::new(config);
174+
self
175+
}
176+
147177
pub fn with_reporter<R: ProgressReporter + Send + Sync + 'static>(
148178
mut self,
149179
reporter: R,
@@ -265,24 +295,18 @@ impl Downloader {
265295
file.lock().await.set_len(file_size).await?;
266296

267297
let mut start = 0;
268-
let thread_size = file_size / threads;
269-
let mut byte_size = thread_size;
270-
271-
//ignore threads if the file is less than a MB.
272-
if file_size < _1MB {
298+
// Determine chunk size: default to per-thread slice, cap at 10 MB for memory,
299+
// or use full file if smaller than 1 MB (no threading benefit).
300+
let chunk_size = if file_size < _1MB {
273301
println!("ℹ️ The file is smaller than 1 MB, so skipping threads.");
274-
byte_size = file_size;
275-
}
276-
277-
// if the byte size is larger than 10 MB, split into 10 MB chunks
278-
// so that memory consumption is less.
279-
if thread_size > _10MB {
280-
byte_size = _10MB
281-
}
302+
file_size
303+
} else {
304+
(file_size / threads).min(self.config.max_chunk_size)
305+
};
282306

283307
// split chunks to download
284308
while start < file_size {
285-
let end = min(start + byte_size, file_size);
309+
let end = min(start + chunk_size, file_size);
286310
self.chunks.lock().await.push(Chunk::new(start, end));
287311
start = end + 1;
288312
}
@@ -308,6 +332,7 @@ impl Downloader {
308332
let url = self.url.clone();
309333
let index_clone = Arc::clone(&index);
310334
let reporter_clone = Arc::clone(&self.reporter);
335+
let config = Arc::clone(&self.config);
311336

312337
let task = tokio::spawn(async move {
313338
let mut worker_total: u64 = 0;
@@ -332,6 +357,7 @@ impl Downloader {
332357
filename: None,
333358
chunks: Arc::clone(&chunks),
334359
reporter: Arc::clone(&reporter_clone),
360+
config: Arc::clone(&config),
335361
};
336362

337363
// Download the chunk and accumulate the bytes downloaded by this worker
@@ -440,4 +466,15 @@ mod tests {
440466
assert_eq!(downloader.file_size, Some(0));
441467
});
442468
}
469+
#[test]
470+
fn test_custom_download_config() {
471+
let config = DownloadConfig::new().set_max_chunk_size(5 * 1024 * 1024);
472+
assert_eq!(config.max_chunk_size, 5 * 1024 * 1024);
473+
}
474+
475+
#[test]
476+
fn test_default_download_config() {
477+
let config = DownloadConfig::new();
478+
assert_eq!(config.max_chunk_size, 10 * 1024 * 1024);
479+
}
443480
}

crates/furl_cli/src/features/cli.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ pub struct FurlCliArgs {
1515
#[arg(short, long)]
1616
pub filename: Option<String>,
1717

18-
/// Number of threads, defaults to 8, maximum allowed 255
19-
#[arg(short, long, default_value_t = 8)]
18+
/// Number of threads, maximum allowed 255
19+
#[arg(short, long, default_value_t = 8, value_parser = clap::value_parser!(u8).range(1..=255))]
2020
pub threads: u8,
21+
22+
/// Number of chunks in MB, maximum allowed 100
23+
#[arg(short, long, default_value_t = 10, value_parser = clap::value_parser!(u8).range(1..=100))]
24+
pub chunksize: u8,
2125
}

crates/furl_cli/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
//! async fn main() {
2121
//! let url = "https://raw.githubusercontent.com/ghimiresdp/furl-cli/refs/heads/main/res/images/example.png";
2222
//! let mut downloader = Downloader::new(url);
23-
//! if let Ok(_) = downloader.download(".", None, Some(4)).await {
23+
//! if let Ok(_) = downloader.download("./downloads/", None, Some(4)).await {
2424
//! println!("Download completed successfully!");
2525
//! } else {
2626
//! println!("Download failed.");

crates/furl_cli/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//!
1111
1212
use clap::Parser;
13+
use furl_core::engine::DownloadConfig;
1314
use furl_core::{Downloader, FurlCliArgs, GraphicalProgressReporter};
1415
use regex::Regex;
1516
use std::process::exit;
@@ -22,6 +23,7 @@ async fn main() {
2223
let path = Path::new(&args.out);
2324
let threads = args.threads;
2425
let filename = args.filename;
26+
let chunk_size = args.chunksize;
2527

2628
if !path.exists() {
2729
println!("The destination path does not exist");
@@ -31,8 +33,13 @@ async fn main() {
3133
// TODO: add extensive url pattern matcher
3234
let re = Regex::new(r"https?://[^\s/$.?#].[^\s]*").unwrap();
3335
if re.captures(&args.url).is_some() {
34-
let mut downloader =
35-
Downloader::new(&args.url).with_reporter(GraphicalProgressReporter::new());
36+
// use config
37+
let download_config =
38+
DownloadConfig::new().set_max_chunk_size(chunk_size as u64 * 1024 * 1024);
39+
40+
let mut downloader = Downloader::new(&args.url)
41+
.with_config(download_config)
42+
.with_reporter(GraphicalProgressReporter::new());
3643
if downloader
3744
.download(&args.out, filename, Some(threads))
3845
.await

0 commit comments

Comments
 (0)