Skip to content

Commit

Permalink
added resume download tests and example
Browse files Browse the repository at this point in the history
  • Loading branch information
LorenzoLeonardo committed Oct 27, 2023
1 parent 7d1d56b commit ff1f13f
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 8 deletions.
68 changes: 67 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ tokio = { version = "1.33", features = ["rt"] }
url = "2.4"

[dev-dependencies]
wiremock = "=0.5.15"
tempdir = "0.3"
futures = "0.3"
tempdir = "0.3"
test-case = "3.2"
wiremock = "=0.5.15"
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,41 @@ async fn main() {
}
}
```

## Resume Downloading a File
```rust
use std::fs;
use std::path::PathBuf;

use async_curl::async_curl::AsyncCurl;
use curl_http_client::{
collector::{Collector, FileInfo},
http_client::{BytesOffset, HttpClient},
request::HttpRequest,
};
use http::{HeaderMap, Method};
use url::Url;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let curl = AsyncCurl::new();
let save_to = PathBuf::from("<FILE PATH TO SAVE>");
let collector = Collector::File(FileInfo::path(save_to.clone()));

let partial_download_file_size = fs::metadata(save_to.as_path())?.len() as usize;
let request = HttpRequest {
url: Url::parse("<SOURCE URL>")?,
method: Method::GET,
headers: HeaderMap::new(),
body: None,
};

let response = HttpClient::new(curl, collector)
.resume_from(BytesOffset::from(partial_download_file_size))?
.request(request)?
.perform()
.await?;
println!("Response: {:?}", response);
Ok(())
}
```
35 changes: 35 additions & 0 deletions examples/resume_download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::fs;
use std::path::PathBuf;

use async_curl::async_curl::AsyncCurl;
use curl_http_client::{
collector::{Collector, FileInfo},
http_client::{BytesOffset, HttpClient},
request::HttpRequest,
};
use http::{HeaderMap, Method};
use url::Url;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let curl = AsyncCurl::new();
let save_to = PathBuf::from("<FILE PATH TO SAVE>");
let collector = Collector::File(FileInfo::path(save_to.clone()));

let partial_download_file_size = fs::metadata(save_to.as_path())?.len() as usize;
let request = HttpRequest {
url: Url::parse("<SOURCE URL>")?,
method: Method::GET,
headers: HeaderMap::new(),
body: None,
};

let response = HttpClient::new(curl, collector)
.resume_from(BytesOffset::from(partial_download_file_size))?
.request(request)?
.perform()
.await?;

println!("Response: {:?}", response);
Ok(())
}
39 changes: 39 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,45 @@
//! }
//! ```
//!
//! # Downloading a File
//! ```rust,no_run
//! use std::fs;
//! use std::path::PathBuf;
//!
//! use async_curl::async_curl::AsyncCurl;
//! use curl_http_client::{
//! collector::{Collector, FileInfo},
//! http_client::{BytesOffset, HttpClient},
//! request::HttpRequest,
//! };
//! use http::{HeaderMap, Method};
//! use url::Url;
//!
//! #[tokio::main(flavor = "current_thread")]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let curl = AsyncCurl::new();
//! let save_to = PathBuf::from("<FILE PATH TO SAVE>");
//! let collector = Collector::File(FileInfo::path(save_to.clone()));
//!
//! let partial_download_file_size = fs::metadata(save_to.as_path())?.len() as usize;
//! let request = HttpRequest {
//! url: Url::parse("<SOURCE URL>")?,
//! method: Method::GET,
//! headers: HeaderMap::new(),
//! body: None,
//! };
//!
//! let response = HttpClient::new(curl, collector)
//! .resume_from(BytesOffset::from(partial_download_file_size))?
//! .request(request)?
//! .perform()
//! .await?;
//!
//! println!("Response: {:?}", response);
//! Ok(())
//! }
//! ```
//!
pub mod collector;
pub mod error;
pub mod http_client;
Expand Down
42 changes: 41 additions & 1 deletion src/test/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use std::fs;

use async_curl::async_curl::AsyncCurl;
use http::{HeaderMap, Method, StatusCode};
use test_case::test_case;
use url::Url;

use crate::collector::{Collector, FileInfo};
use crate::http_client::{BytesPerSec, HttpClient};
use crate::http_client::{BytesOffset, BytesPerSec, HttpClient};
use crate::request::HttpRequest;
use crate::test::test_setup::{setup_test_environment, MockResponder, ResponderType};

Expand Down Expand Up @@ -66,3 +67,42 @@ async fn test_download_with_speed_control() {
assert_eq!(response.body, None);
assert_eq!(fs::read(save_to).unwrap(), include_bytes!("sample.jpg"));
}

#[test_case(4500, StatusCode::PARTIAL_CONTENT; "Offset 4500 bytes")]
#[test_case(0, StatusCode::OK ; "Offset 0 bytes")]
#[test_case(include_bytes!("sample.jpg").len(), StatusCode::PARTIAL_CONTENT ; "Offset max bytes")]
#[tokio::test]
async fn test_resume_download(offset: usize, expected_status_code: StatusCode) {
let responder = MockResponder::new(ResponderType::File);
let (server, tempdir) = setup_test_environment(responder).await;
let target_url = Url::parse(format!("{}/test", server.uri()).as_str()).unwrap();

let save_to = tempdir.path().join("downloaded_file.jpg");

let partial_saved_file = include_bytes!("sample.jpg");
fs::write(save_to.as_path(), &partial_saved_file[0..offset]).unwrap();

let partial_file_size = fs::metadata(save_to.as_path()).unwrap().len() as usize;

let curl = AsyncCurl::new();
let collector = Collector::File(FileInfo::path(save_to.clone()));
let request = HttpRequest {
url: target_url,
method: Method::GET,
headers: HeaderMap::new(),
body: None,
};
let response = HttpClient::new(curl, collector)
.resume_from(BytesOffset::from(partial_file_size))
.unwrap()
.request(request)
.unwrap()
.perform()
.await
.unwrap();

println!("Response: {:?}", response);
assert_eq!(response.status_code, expected_status_code);
assert_eq!(response.body, None);
assert_eq!(fs::read(save_to).unwrap(), include_bytes!("sample.jpg"));
}
59 changes: 55 additions & 4 deletions src/test/test_setup.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::str::FromStr;

use http::StatusCode;
use tempdir::TempDir;
use wiremock::{
http::Method, matchers::path, Mock, MockServer, Request, Respond, ResponseTemplate,
http::{HeaderName, HeaderValue, HeaderValues, Method},
matchers::path,
Mock, MockServer, Request, Respond, ResponseTemplate,
};

pub enum ResponderType {
Expand All @@ -20,12 +24,45 @@ impl MockResponder {

impl Respond for MockResponder {
fn respond(&self, request: &Request) -> ResponseTemplate {
//println!("Request: {:?}", request);
match request.method {
Method::Get => match &self.responder {
ResponderType::File => {
let contents = include_bytes!("sample.jpg");
ResponseTemplate::new(StatusCode::OK).set_body_bytes(contents.as_slice())
let mock_file = include_bytes!("sample.jpg");
let header_name = HeaderName::from_str("range").unwrap();
let total_file_size = mock_file.len();
println!("Request: {:?}", request);
if let Some(value) = request.headers.get(&header_name) {
let offset = parse_range(value).unwrap() as usize;
println!("Offset: {}", offset);

let content_length = format!("{}", total_file_size - offset);
println!("Content-Length: {}", content_length);
let content_range = format!(
"bytes {}-{}/{}",
offset,
total_file_size - 1,
total_file_size
);
println!("Content-Range: {}", content_range);

ResponseTemplate::new(StatusCode::PARTIAL_CONTENT)
.append_header(
HeaderName::from_str("Content-Range").unwrap(),
HeaderValue::from_str(content_range.as_str()).unwrap(),
)
.append_header(
HeaderName::from_str("Content-Length").unwrap(),
HeaderValue::from_str(content_length.as_str()).unwrap(),
)
.append_header(
HeaderName::from_str("Accept-Ranges").unwrap(),
HeaderValue::from_str("bytes").unwrap(),
)
.set_body_bytes(&mock_file[offset..])
} else {
let contents = include_bytes!("sample.jpg");
ResponseTemplate::new(StatusCode::OK).set_body_bytes(contents.as_slice())
}
}
ResponderType::Body(body) => {
ResponseTemplate::new(StatusCode::OK).set_body_bytes(body.as_slice())
Expand Down Expand Up @@ -55,6 +92,20 @@ impl Respond for MockResponder {
}
}

fn parse_range(input: &HeaderValues) -> Option<u64> {
let input = input.to_string();
if let Some(start_pos) = input.find('=') {
if let Some(end_pos) = input.rfind('-') {
let numeric_value = &input[start_pos + 1..end_pos];
numeric_value.parse::<u64>().ok()
} else {
None
}
} else {
None
}
}

pub async fn setup_test_environment(responder: MockResponder) -> (MockServer, TempDir) {
let mock_server = MockServer::start().await;
let tempdir = TempDir::new_in("./", "test").unwrap();
Expand Down

0 comments on commit ff1f13f

Please sign in to comment.