@@ -4,9 +4,34 @@ use anyhow::{ensure, Context, Result};
44use sha2:: Digest ;
55use tokio:: io:: AsyncWriteExt ;
66
7+ /// Describes the naming convention that `verified_download` is permitted
8+ /// to assume in the directory it saves downloads to.
9+ ///
10+ /// Consumers (direct or indirect) of `verified_download` are expected to check
11+ /// if the file is already downloaded before calling it. This enum exists
12+ /// to address race conditions when the same blob is downloaded several times
13+ /// concurrently.
14+ ///
15+ /// The significance of this is for when the destination file turns out to already
16+ /// exist after all (that is, has been created since the caller originally checked
17+ /// existence). In the ContentIndexed case, the name already existing guarantees that
18+ /// the file matches the download. If a caller uses `verified_download` for a
19+ /// *non*-content-indexed case then they must provide and handle a new variant
20+ /// of the enum.
21+ pub enum DestinationConvention {
22+ /// The download destination is content-indexed; therefore, in the event
23+ /// of a race, the loser of the race can be safely discarded.
24+ ContentIndexed ,
25+ }
26+
727/// Downloads content from `url` which will be verified to match `digest` and
828/// then moved to `dest`.
9- pub async fn verified_download ( url : & str , digest : & str , dest : & Path ) -> Result < ( ) > {
29+ pub async fn verified_download (
30+ url : & str ,
31+ digest : & str ,
32+ dest : & Path ,
33+ convention : DestinationConvention ,
34+ ) -> Result < ( ) > {
1035 tracing:: debug!( "Downloading content from {url:?}" ) ;
1136
1237 // Prepare tempfile destination
@@ -38,9 +63,16 @@ pub async fn verified_download(url: &str, digest: &str, dest: &Path) -> Result<(
3863 ) ;
3964
4065 // Move to final destination
41- temp_path
42- . persist_noclobber ( dest)
43- . with_context ( || format ! ( "Failed to save download from {url} to {}" , dest. display( ) ) ) ?;
66+ let persist_result = temp_path. persist_noclobber ( dest) ;
4467
45- Ok ( ( ) )
68+ persist_result. or_else ( |e| {
69+ let file_already_exists = e. error . kind ( ) == std:: io:: ErrorKind :: AlreadyExists ;
70+ if file_already_exists && matches ! ( convention, DestinationConvention :: ContentIndexed ) {
71+ Ok ( ( ) )
72+ } else {
73+ Err ( e) . with_context ( || {
74+ format ! ( "Failed to save download from {url} to {}" , dest. display( ) )
75+ } )
76+ }
77+ } )
4678}
0 commit comments