Skip to content

Commit 8c481c8

Browse files
committed
Auto merge of #1020 - jelford:support_partial_downloads, r=brson
Support partial downloads This PR should close #889 A couple of implementation details: Only added support for the curl backend; previously discussed that there's an intention to get rid of rustup's own download code, and the default feature-set uses curl anyway, so hopefully this is okay. Added new testing to the download crate - while it's there, it makes sense to have a test. Since using curl's "resume" functionality, I figured it's probably fine to just file:// urls for test cases. Previously tested using a small hyper-based http server, but that feels like overkill. For hashing files, I've set the buffer size to 2^15 - just because that's what strace tells me is used by `sha256sum` on my local PC. It seems much slower than that command though, and it's not obvious why, so maybe I've done something silly here. Finally, and maybe most controversially, I haven't done anything about cleaning up aborted partials. I don't really know when a good time is to do this, but a couple of suggestions that I'd be happy to implement: * Every run, just check the download cache for any files > 7 days old and smoke them * On self-update, as that seems like a natural time for generic "maintenance" sorts of operations I mentioned in my last PR, but the same disclaimer: I haven't written much rust, so I fully expect you will see some problems (also very happy to accept style criticisms). I accidentally ran a `rustfmt` on some things so apologies for the noise (can revert but... maybe it's worth having anyway?).
2 parents 04fda5e + c63b275 commit 8c481c8

File tree

10 files changed

+243
-51
lines changed

10 files changed

+243
-51
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ci/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ cargo -vV
1010
cargo build --release --target $TARGET
1111

1212
if [ -z "$SKIP_TESTS" ]; then
13+
cargo test --release -p download --target $TARGET
1314
cargo test --release -p rustup-dist --target $TARGET
1415
cargo test --release --target $TARGET
1516
fi

src/download/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ curl = { version = "0.4", optional = true }
2121
lazy_static = { version = "0.2", optional = true }
2222
native-tls = { version = "0.1", optional = true }
2323

24+
[dev-dependencies]
25+
tempdir = "0.3.4"
26+
2427
[dependencies.hyper]
2528
version = "0.9.8"
2629
default-features = false

src/download/src/errors.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
2+
13
error_chain! {
24
links { }
35

4-
foreign_links { }
6+
foreign_links {
7+
Io(::std::io::Error);
8+
}
59

610
errors {
711
HttpStatus(e: u32) {

src/download/src/lib.rs

Lines changed: 82 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub enum Backend { Curl, Hyper, Rustls }
2121

2222
#[derive(Debug, Copy, Clone)]
2323
pub enum Event<'a> {
24+
ResumingPartialDownload,
2425
/// Received the Content-Length of the to-be downloaded data.
2526
DownloadContentLengthReceived(u64),
2627
/// Received some data.
@@ -33,47 +34,89 @@ const BACKENDS: &'static [Backend] = &[
3334
Backend::Rustls
3435
];
3536

36-
pub fn download(url: &Url,
37-
callback: &Fn(Event) -> Result<()>)
38-
-> Result<()> {
39-
for &backend in BACKENDS {
40-
match download_with_backend(backend, url, callback) {
41-
Err(Error(ErrorKind::BackendUnavailable(_), _)) => (),
42-
Err(e) => return Err(e),
43-
Ok(()) => return Ok(()),
44-
}
45-
}
46-
47-
Err("no working backends".into())
48-
}
4937

50-
pub fn download_with_backend(backend: Backend,
38+
fn download_with_backend(backend: Backend,
5139
url: &Url,
40+
resume_from: u64,
5241
callback: &Fn(Event) -> Result<()>)
5342
-> Result<()> {
5443
match backend {
55-
Backend::Curl => curl::download(url, callback),
44+
Backend::Curl => curl::download(url, resume_from, callback),
5645
Backend::Hyper => hyper::download(url, callback),
5746
Backend::Rustls => rustls::download(url, callback),
5847
}
5948
}
6049

50+
fn supports_partial_download(backend: &Backend) -> bool {
51+
match backend {
52+
&Backend::Curl => true,
53+
_ => false
54+
}
55+
}
56+
6157
pub fn download_to_path_with_backend(
6258
backend: Backend,
6359
url: &Url,
6460
path: &Path,
61+
resume_from_partial: bool,
6562
callback: Option<&Fn(Event) -> Result<()>>)
6663
-> Result<()>
6764
{
6865
use std::cell::RefCell;
69-
use std::fs::{self, File};
70-
use std::io::Write;
66+
use std::fs::{OpenOptions};
67+
use std::io::{Read, Write, Seek, SeekFrom};
7168

7269
|| -> Result<()> {
73-
let file = RefCell::new(try!(File::create(&path).chain_err(
74-
|| "error creating file for download")));
70+
let (file, resume_from) = if resume_from_partial && supports_partial_download(&backend) {
71+
let possible_partial = OpenOptions::new()
72+
.read(true)
73+
.open(&path);
74+
75+
let downloaded_so_far = if let Ok(mut partial) = possible_partial {
76+
if let Some(cb) = callback {
77+
try!(cb(Event::ResumingPartialDownload));
78+
79+
let mut buf = vec![0; 32768];
80+
let mut downloaded_so_far = 0;
81+
loop {
82+
let n = try!(partial.read(&mut buf));
83+
downloaded_so_far += n as u64;
84+
if n == 0 {
85+
break;
86+
}
87+
try!(cb(Event::DownloadDataReceived(&buf[..n])));
88+
}
89+
90+
downloaded_so_far
91+
} else {
92+
let file_info = try!(partial.metadata());
93+
file_info.len()
94+
}
95+
} else {
96+
0
97+
};
7598

76-
try!(download_with_backend(backend, url, &|event| {
99+
let mut possible_partial =
100+
try!(OpenOptions::new()
101+
.write(true)
102+
.create(true)
103+
.open(&path)
104+
.chain_err(|| "error opening file for download"));
105+
106+
try!(possible_partial.seek(SeekFrom::End(0)));
107+
108+
(possible_partial, downloaded_so_far)
109+
} else {
110+
(try!(OpenOptions::new()
111+
.write(true)
112+
.create(true)
113+
.open(&path)
114+
.chain_err(|| "error creating file for download")), 0)
115+
};
116+
117+
let file = RefCell::new(file);
118+
119+
try!(download_with_backend(backend, url, resume_from, &|event| {
77120
if let Event::DownloadDataReceived(data) = event {
78121
try!(file.borrow_mut().write_all(data)
79122
.chain_err(|| "unable to write download to disk"));
@@ -89,11 +132,8 @@ pub fn download_to_path_with_backend(
89132

90133
Ok(())
91134
}().map_err(|e| {
92-
if path.is_file() {
93-
// FIXME ignoring compound errors
94-
let _ = fs::remove_file(path);
95-
}
96135

136+
// TODO is there any point clearing up here? What kind of errors will leave us with an unusable partial?
97137
e
98138
})
99139
}
@@ -114,6 +154,7 @@ pub mod curl {
114154
use super::Event;
115155

116156
pub fn download(url: &Url,
157+
resume_from: u64,
117158
callback: &Fn(Event) -> Result<()> )
118159
-> Result<()> {
119160
// Fetch either a cached libcurl handle (which will preserve open
@@ -128,6 +169,15 @@ pub mod curl {
128169
try!(handle.url(&url.to_string()).chain_err(|| "failed to set url"));
129170
try!(handle.follow_location(true).chain_err(|| "failed to set follow redirects"));
130171

172+
if resume_from > 0 {
173+
try!(handle.resume_from(resume_from)
174+
.chain_err(|| "setting the range header for download resumption"));
175+
} else {
176+
// an error here indicates that the range header isn't supported by underlying curl,
177+
// so there's nothing to "clear" - safe to ignore this error.
178+
let _ = handle.resume_from(0);
179+
}
180+
131181
// Take at most 30s to connect
132182
try!(handle.connect_timeout(Duration::new(30, 0)).chain_err(|| "failed to set connect timeout"));
133183

@@ -154,8 +204,8 @@ pub mod curl {
154204
if let Ok(data) = str::from_utf8(header) {
155205
let prefix = "Content-Length: ";
156206
if data.starts_with(prefix) {
157-
if let Ok(s) = data[prefix.len()..].trim().parse() {
158-
let msg = Event::DownloadContentLengthReceived(s);
207+
if let Ok(s) = data[prefix.len()..].trim().parse::<u64>() {
208+
let msg = Event::DownloadContentLengthReceived(s + resume_from);
159209
match callback(msg) {
160210
Ok(()) => (),
161211
Err(e) => {
@@ -188,11 +238,12 @@ pub mod curl {
188238
}));
189239
}
190240

191-
// If we didn't get a 200 or 0 ("OK" for files) then return an error
241+
// If we didn't get a 20x or 0 ("OK" for files) then return an error
192242
let code = try!(handle.response_code().chain_err(|| "failed to get response code"));
193-
if code != 200 && code != 0 {
194-
return Err(ErrorKind::HttpStatus(code).into());
195-
}
243+
match code {
244+
0 | 200 ... 299 => {},
245+
_ => { return Err(ErrorKind::HttpStatus(code).into()); }
246+
};
196247

197248
Ok(())
198249
})
@@ -639,6 +690,7 @@ pub mod curl {
639690
use super::Event;
640691

641692
pub fn download(_url: &Url,
693+
_resume_from: u64,
642694
_callback: &Fn(Event) -> Result<()> )
643695
-> Result<()> {
644696
Err(ErrorKind::BackendUnavailable("curl").into())
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#![cfg(feature = "curl-backend")]
2+
3+
extern crate download;
4+
extern crate tempdir;
5+
extern crate url;
6+
7+
use std::sync::{Arc, Mutex};
8+
use std::fs::{self, File};
9+
use std::io::{self, Read};
10+
use std::path::Path;
11+
12+
use tempdir::TempDir;
13+
use url::Url;
14+
15+
use download::*;
16+
17+
fn tmp_dir() -> TempDir {
18+
TempDir::new("rustup-download-test-").expect("creating tempdir for test")
19+
}
20+
21+
fn file_contents(path: &Path) -> String {
22+
let mut result = String::new();
23+
File::open(&path).unwrap().read_to_string(&mut result).expect("reading test result file");
24+
result
25+
}
26+
27+
28+
pub fn write_file(path: &Path, contents: &str) {
29+
let mut file = fs::OpenOptions::new()
30+
.write(true)
31+
.truncate(true)
32+
.create(true)
33+
.open(path)
34+
.expect("writing test data");
35+
36+
io::Write::write_all(&mut file, contents.as_bytes()).expect("writing test data");
37+
38+
file.sync_data().expect("writing test data");
39+
}
40+
41+
#[test]
42+
fn partially_downloaded_file_gets_resumed_from_byte_offset() {
43+
let tmpdir = tmp_dir();
44+
let from_path = tmpdir.path().join("download-source");
45+
write_file(&from_path, "xxx45");
46+
47+
let target_path = tmpdir.path().join("downloaded");
48+
write_file(&target_path, "123");
49+
50+
let from_url = Url::from_file_path(&from_path).unwrap();
51+
download_to_path_with_backend(
52+
Backend::Curl,
53+
&from_url,
54+
&target_path,
55+
true,
56+
None)
57+
.expect("Test download failed");
58+
59+
assert_eq!(file_contents(&target_path), "12345");
60+
}
61+
62+
#[test]
63+
fn callback_gets_all_data_as_if_the_download_happened_all_at_once() {
64+
let tmpdir = tmp_dir();
65+
66+
let from_path = tmpdir.path().join("download-source");
67+
write_file(&from_path, "xxx45");
68+
69+
let target_path = tmpdir.path().join("downloaded");
70+
write_file(&target_path, "123");
71+
72+
let from_url = Url::from_file_path(&from_path).unwrap();
73+
74+
let received_in_callback = Arc::new(Mutex::new(Vec::new()));
75+
76+
download_to_path_with_backend(Backend::Curl,
77+
&from_url,
78+
&target_path,
79+
true,
80+
Some(&|msg| {
81+
match msg {
82+
Event::DownloadDataReceived(data) => {
83+
for b in data.iter() {
84+
received_in_callback.lock().unwrap().push(b.clone());
85+
}
86+
}
87+
_ => {}
88+
}
89+
90+
91+
Ok(())
92+
}))
93+
.expect("Test download failed");
94+
95+
let ref observed_bytes = *received_in_callback.lock().unwrap();
96+
assert_eq!(observed_bytes, &vec![b'1', b'2', b'3', b'4', b'5']);
97+
assert_eq!(file_contents(&target_path), "12345");
98+
}

0 commit comments

Comments
 (0)