Skip to content
This repository was archived by the owner on Jun 20, 2022. It is now read-only.

Commit e5e3a15

Browse files
committed
Support resumption of partial downloads
* Adds support only for the Curl backend, which is the default anyway. * In order to distinguish between a file that has been fully downloaded (but not used yet) and should therefore be hash-checked vs. those that require more data, store partials with a .partial extension. * Adds a simple http-server to rustup-mock, to allow the download module to be properly tested. It's not clear how to easily emulate a server that stops half-way without that. The tests for the overall download-resumption functionality should be fairly re-usable if we migrate to another download solution in the future (e.g. in rust-lang#993) * Don't bother with resumption for meta-data files, since they're likely to go out of date anyway.
1 parent 2ce8d72 commit e5e3a15

File tree

11 files changed

+567
-220
lines changed

11 files changed

+567
-220
lines changed

Cargo.lock

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

src/download/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ 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+
rustup-mock = { path = "../rustup-mock", version = "1.0.0" }
26+
tempdir = "0.3.4"
27+
2428
[dependencies.hyper]
2529
version = "0.9.8"
2630
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+
use std::io;
2+
13
error_chain! {
24
links { }
35

4-
foreign_links { }
6+
foreign_links {
7+
Io(::std::io::Error) #[cfg(unix)];
8+
}
59

610
errors {
711
HttpStatus(e: u32) {

src/download/src/lib.rs

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,47 +33,86 @@ const BACKENDS: &'static [Backend] = &[
3333
Backend::Rustls
3434
];
3535

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-
}
4636

47-
Err("no working backends".into())
48-
}
49-
50-
pub fn download_with_backend(backend: Backend,
37+
fn download_with_backend(backend: Backend,
5138
url: &Url,
39+
resume_from: u64,
5240
callback: &Fn(Event) -> Result<()>)
5341
-> Result<()> {
5442
match backend {
55-
Backend::Curl => curl::download(url, callback),
43+
Backend::Curl => curl::download(url, resume_from, callback),
5644
Backend::Hyper => hyper::download(url, callback),
5745
Backend::Rustls => rustls::download(url, callback),
5846
}
5947
}
6048

49+
fn supports_partial_download(backend: &Backend) -> bool {
50+
match backend {
51+
&Backend::Curl => true,
52+
_ => false
53+
}
54+
}
55+
6156
pub fn download_to_path_with_backend(
6257
backend: Backend,
6358
url: &Url,
6459
path: &Path,
60+
resume_from_partial: bool,
6561
callback: Option<&Fn(Event) -> Result<()>>)
6662
-> Result<()>
6763
{
6864
use std::cell::RefCell;
69-
use std::fs::{self, File};
70-
use std::io::Write;
65+
use std::fs::{self, File, OpenOptions};
66+
use std::io::{Read, Write, Seek, SeekFrom};
7167

7268
|| -> Result<()> {
73-
let file = RefCell::new(try!(File::create(&path).chain_err(
74-
|| "error creating file for download")));
69+
let (file, resume_from) = if resume_from_partial && supports_partial_download(&backend) {
70+
let mut possible_partial = OpenOptions::new()
71+
.read(true)
72+
.open(&path);
73+
74+
let downloaded_so_far = if let Ok(mut partial) = possible_partial {
75+
if let Some(cb) = callback {
76+
println!("Reading file in {}", path.display());
77+
let mut buf = vec![0; 1024*1024*10];
78+
let mut downloaded_so_far = 0;
79+
let mut number_of_reads = 0;
80+
loop {
81+
let n = try!(partial.read(&mut buf));
82+
downloaded_so_far += n as u64;
83+
number_of_reads += 1;
84+
if n == 0 {
85+
println!("nothing read after {} reads (accumulated {})", number_of_reads, downloaded_so_far);
86+
break;
87+
}
88+
try!(cb(Event::DownloadDataReceived(&buf[..n])));
89+
}
90+
downloaded_so_far
91+
} else {
92+
use std::fs::Metadata;
93+
let file_info = try!(partial.metadata());
94+
file_info.len()
95+
}
96+
} else {
97+
0
98+
};
99+
100+
let mut possible_partial = try!(OpenOptions::new().write(true).create(true).open(&path).chain_err(|| "error opening file for download"));
101+
try!(possible_partial.seek(SeekFrom::End(0)));
102+
103+
(possible_partial, downloaded_so_far)
104+
} else {
105+
println!("Download resume not supported");
106+
(try!(OpenOptions::new()
107+
.write(true)
108+
.create(true)
109+
.open(&path)
110+
.chain_err(|| "error creating file for download")), 0)
111+
};
75112

76-
try!(download_with_backend(backend, url, &|event| {
113+
let file = RefCell::new(file);
114+
115+
try!(download_with_backend(backend, url, resume_from, &|event| {
77116
if let Event::DownloadDataReceived(data) = event {
78117
try!(file.borrow_mut().write_all(data)
79118
.chain_err(|| "unable to write download to disk"));
@@ -89,11 +128,8 @@ pub fn download_to_path_with_backend(
89128

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

132+
// TODO is there any point clearing up here? What kind of errors will leave us with an unusable partial?
97133
e
98134
})
99135
}
@@ -114,6 +150,7 @@ pub mod curl {
114150
use super::Event;
115151

116152
pub fn download(url: &Url,
153+
resume_from: u64,
117154
callback: &Fn(Event) -> Result<()> )
118155
-> Result<()> {
119156
// Fetch either a cached libcurl handle (which will preserve open
@@ -128,6 +165,12 @@ pub mod curl {
128165
try!(handle.url(&url.to_string()).chain_err(|| "failed to set url"));
129166
try!(handle.follow_location(true).chain_err(|| "failed to set follow redirects"));
130167

168+
if resume_from > 0 {
169+
try!(handle.range(&(resume_from.to_string() + "-")).chain_err(|| "setting the range-header for download resumption"));
170+
} else {
171+
try!(handle.range("").chain_err(|| "clearing range header"));
172+
}
173+
131174
// Take at most 30s to connect
132175
try!(handle.connect_timeout(Duration::new(30, 0)).chain_err(|| "failed to set connect timeout"));
133176

@@ -154,8 +197,8 @@ pub mod curl {
154197
if let Ok(data) = str::from_utf8(header) {
155198
let prefix = "Content-Length: ";
156199
if data.starts_with(prefix) {
157-
if let Ok(s) = data[prefix.len()..].trim().parse() {
158-
let msg = Event::DownloadContentLengthReceived(s);
200+
if let Ok(s) = data[prefix.len()..].trim().parse::<u64>() {
201+
let msg = Event::DownloadContentLengthReceived(s + resume_from);
159202
match callback(msg) {
160203
Ok(()) => (),
161204
Err(e) => {
@@ -188,10 +231,11 @@ pub mod curl {
188231
}));
189232
}
190233

191-
// If we didn't get a 200 or 0 ("OK" for files) then return an error
234+
// If we didn't get a 20x or 0 ("OK" for files) then return an error
192235
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());
236+
match code {
237+
0 | 200...299 => { println!("status code: {}", code)}
238+
_ => { return Err(ErrorKind::HttpStatus(code).into()); }
195239
}
196240

197241
Ok(())
@@ -639,6 +683,7 @@ pub mod curl {
639683
use super::Event;
640684

641685
pub fn download(_url: &Url,
686+
_resume_from: u64,
642687
_callback: &Fn(Event) -> Result<()> )
643688
-> Result<()> {
644689
Err(ErrorKind::BackendUnavailable("curl").into())
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#![cfg(feature = "curl-backend")]
2+
3+
extern crate download;
4+
extern crate rustup_mock;
5+
extern crate tempdir;
6+
7+
use std::fs::{self, File};
8+
use std::io::{Read};
9+
use std::path::Path;
10+
11+
use tempdir::TempDir;
12+
13+
use rustup_mock::http_server;
14+
15+
use download::*;
16+
17+
fn setup(test: &Fn(TempDir, http_server::Server) -> ()) {
18+
let tmp = TempDir::new("rustup-download-test-").expect("creating tempdir for test");
19+
let served_dir = &tmp.path().join("test-files");
20+
fs::DirBuilder::new().create(served_dir).expect("setting up a folder to server files from");
21+
let server = http_server::Server::serve_from(served_dir).expect("setting up http server for test");
22+
test(tmp, server);
23+
}
24+
25+
fn file_contents(path: &Path) -> String {
26+
let mut result = String::new();
27+
File::open(&path).unwrap().read_to_string(&mut result).expect("reading test result file");
28+
result
29+
}
30+
31+
#[test]
32+
fn when_download_is_interrupted_partial_file_is_left_on_disk() {
33+
setup(&|tmpdir, mut server| {
34+
let target_path = tmpdir.path().join("downloaded");
35+
36+
server.put_file_from_bytes("test-file", b"12345");
37+
38+
server.stop_after_bytes(3);
39+
download_to_path_with_backend(
40+
Backend::Curl, &server.address().join("test-file").unwrap(), &target_path, true, None)
41+
.expect("Test download failed");
42+
43+
assert_eq!(file_contents(&target_path), "123");
44+
});
45+
}
46+
47+
#[test]
48+
fn download_interrupted_and_resumed() {
49+
setup(&|tmpdir, mut server| {
50+
let target_path = tmpdir.path().join("downloaded");
51+
52+
server.put_file_from_bytes("test-file", b"12345");
53+
54+
server.stop_after_bytes(3);
55+
download_to_path_with_backend(
56+
Backend::Curl, &server.address().join("test-file").unwrap(), &target_path, true, None)
57+
.expect("Test download failed");
58+
59+
server.stop_after_bytes(2);
60+
download_to_path_with_backend(
61+
Backend::Curl, &server.address().join("test-file").unwrap(), &target_path, true, None)
62+
.expect("Test download failed");
63+
64+
assert_eq!(file_contents(&target_path), "12345");
65+
});
66+
}
67+
68+
#[test]
69+
fn resuming_download_with_callback_that_needs_to_read_contents() {
70+
setup(&|tmpdir, mut server| {
71+
let target_path = tmpdir.path().join("downloaded");
72+
73+
server.put_file_from_bytes("test-file", b"12345");
74+
75+
server.stop_after_bytes(3);
76+
download_to_path_with_backend(
77+
Backend::Curl, &server.address().join("test-file").unwrap(), &target_path, true, Some(&|_| {Ok(())}))
78+
.expect("Test download failed");
79+
80+
server.stop_after_bytes(2);
81+
download_to_path_with_backend(
82+
Backend::Curl, &server.address().join("test-file").unwrap(), &target_path, true, Some(&|_| {Ok(())}))
83+
.expect("Test download failed");
84+
85+
assert_eq!(file_contents(&target_path), "12345");
86+
});
87+
}

0 commit comments

Comments
 (0)