@@ -778,6 +778,55 @@ fn spawn_file_encryption(path: PathBuf) -> Result<EncryptionChannels> {
778778 Ok ( ( chunk_rx, datamap_rx, handle) )
779779}
780780
781+ /// RAII guard for the staging temp file used during a disk download.
782+ ///
783+ /// Removes the file on drop — including a panic unwind out of the
784+ /// `block_in_place` decrypt loop — unless [`commit`](Self::commit) has
785+ /// promoted it to its final path. Centralizes the cleanup the explicit error
786+ /// arms used to repeat.
787+ struct TempDownload {
788+ /// `Some` while the staging file may need cleanup; `None` once committed.
789+ path : Option < PathBuf > ,
790+ }
791+
792+ impl TempDownload {
793+ fn new ( path : PathBuf ) -> Self {
794+ Self { path : Some ( path) }
795+ }
796+
797+ /// Path of the staging file (valid until `commit`).
798+ fn path ( & self ) -> & Path {
799+ self . path
800+ . as_deref ( )
801+ . expect ( "TempDownload::path called after commit" )
802+ }
803+
804+ /// Rename the staged file to `dest`. On success the guard is defused so
805+ /// `Drop` is a no-op; on failure the guard stays armed and `Drop` removes
806+ /// the orphaned temp file.
807+ fn commit ( mut self , dest : & Path ) -> std:: io:: Result < ( ) > {
808+ std:: fs:: rename ( self . path ( ) , dest) ?; // err → guard armed → Drop cleans up
809+ self . path = None ; // success → nothing left to clean
810+ Ok ( ( ) )
811+ }
812+ }
813+
814+ impl Drop for TempDownload {
815+ fn drop ( & mut self ) {
816+ if let Some ( path) = self . path . take ( ) {
817+ if let Err ( e) = std:: fs:: remove_file ( & path) {
818+ // Absent file is fine (never created / already gone).
819+ if e. kind ( ) != std:: io:: ErrorKind :: NotFound {
820+ warn ! (
821+ "Failed to remove temp download file {}: {e}" ,
822+ path. display( )
823+ ) ;
824+ }
825+ }
826+ }
827+ }
828+ }
829+
781830impl Client {
782831 /// Upload a file to the network using streaming self-encryption.
783832 ///
@@ -2189,7 +2238,6 @@ impl Client {
21892238 ///
21902239 /// Returns an error if any chunk cannot be retrieved, decryption fails,
21912240 /// or the file cannot be written.
2192- #[ allow( clippy:: unused_async) ]
21932241 pub async fn file_download ( & self , data_map : & DataMap , output : & Path ) -> Result < u64 > {
21942242 self . file_download_with_progress ( data_map, output, None )
21952243 . await
@@ -2574,7 +2622,8 @@ impl Client {
25742622 ///
25752623 /// Same as [`Client::file_download`] but sends [`DownloadEvent`]s for UI
25762624 /// feedback. Streams to a temp file (one decrypt batch resident at a time)
2577- /// and renames atomically on success.
2625+ /// and renames atomically on success. A [`TempDownload`] guard removes the
2626+ /// staging file on any error path, including a panic.
25782627 pub async fn file_download_with_progress (
25792628 & self ,
25802629 data_map : & DataMap ,
@@ -2587,47 +2636,28 @@ impl Client {
25872636 let unique: u64 = rand:: random ( ) ;
25882637 let tmp_path = parent. join ( format ! ( ".ant_download_{}_{unique}.tmp" , std:: process:: id( ) ) ) ;
25892638
2590- let mut file = std:: fs:: File :: create ( & tmp_path) ?;
2591- let write_result = self
2639+ // Guard removes the staging file on any early return OR a panic unwind
2640+ // out of the `block_in_place` decrypt loop; defused only by a
2641+ // successful commit(). Centralizes what used to be three duplicated
2642+ // cleanup arms.
2643+ let tmp = TempDownload :: new ( tmp_path) ;
2644+ let mut file = std:: fs:: File :: create ( tmp. path ( ) ) ?;
2645+
2646+ let bytes_written = self
25922647 . download_decrypted_chunks ( data_map, progress, |bytes| {
25932648 let r = file. write_all ( & bytes) . map_err ( Error :: from) ;
25942649 std:: future:: ready ( r)
25952650 } )
2596- . await
2597- . and_then ( |bytes_written| {
2598- file. flush ( ) ?;
2599- Ok ( bytes_written)
2600- } ) ;
2651+ . await ?;
2652+ file. flush ( ) ?;
2653+ drop ( file) ; // close the handle before rename (Windows won't rename an open file)
26012654
2602- match write_result {
2603- Ok ( bytes_written) => match std:: fs:: rename ( & tmp_path, output) {
2604- Ok ( ( ) ) => {
2605- info ! (
2606- "File downloaded: {bytes_written} bytes written to {}" ,
2607- output. display( )
2608- ) ;
2609- Ok ( bytes_written)
2610- }
2611- Err ( rename_err) => {
2612- if let Err ( cleanup_err) = std:: fs:: remove_file ( & tmp_path) {
2613- warn ! (
2614- "Failed to remove temp download file {}: {cleanup_err}" ,
2615- tmp_path. display( )
2616- ) ;
2617- }
2618- Err ( rename_err. into ( ) )
2619- }
2620- } ,
2621- Err ( e) => {
2622- if let Err ( cleanup_err) = std:: fs:: remove_file ( & tmp_path) {
2623- warn ! (
2624- "Failed to remove temp download file {}: {cleanup_err}" ,
2625- tmp_path. display( )
2626- ) ;
2627- }
2628- Err ( e)
2629- }
2630- }
2655+ tmp. commit ( output) ?;
2656+ info ! (
2657+ "File downloaded: {bytes_written} bytes written to {}" ,
2658+ output. display( )
2659+ ) ;
2660+ Ok ( bytes_written)
26312661 }
26322662
26332663 /// Download and decrypt a file, streaming the plaintext to `sink` instead
@@ -2637,7 +2667,14 @@ impl Client {
26372667 /// receives bytes progressively as each batch decrypts, suitable for
26382668 /// forwarding to an HTTP chunked body or a gRPC response stream. The
26392669 /// bounded `sink` applies backpressure. If the receiver is dropped (e.g.
2640- /// the client disconnected) the download stops early and returns an error.
2670+ /// the client disconnected) the download stops early and returns
2671+ /// [`Error::Cancelled`].
2672+ ///
2673+ /// The channel item type is `Result<Bytes, Error>`, so the caller sets up:
2674+ ///
2675+ /// ```ignore
2676+ /// let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, Error>>(8);
2677+ /// ```
26412678 ///
26422679 /// Typically the caller `tokio::spawn`s this and converts the matching
26432680 /// `Receiver` into its response stream. Requires a multi-threaded Tokio
@@ -2653,7 +2690,7 @@ impl Client {
26532690 async move {
26542691 sink. send ( Ok ( bytes) )
26552692 . await
2656- . map_err ( |_| Error :: Network ( "download stream receiver dropped" . into ( ) ) )
2693+ . map_err ( |_| Error :: Cancelled ( "download stream receiver dropped" . into ( ) ) )
26572694 }
26582695 } )
26592696 . await
0 commit comments