viewpoint_core/page/download/
mod.rs

1//! Download handling for browser downloads.
2//!
3//! This module provides functionality for handling file downloads.
4
5// Allow dead code for scaffolding that will be wired up in future
6
7use std::path::{Path, PathBuf};
8
9use tokio::sync::watch;
10use tracing::{debug, instrument};
11
12use crate::error::NetworkError;
13
14/// Download progress state.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DownloadState {
17    /// Download is in progress.
18    InProgress,
19    /// Download completed successfully.
20    Completed,
21    /// Download was canceled.
22    Canceled,
23}
24
25/// A file download.
26///
27/// Downloads are emitted via the `page.on_download()` callback or can be
28/// obtained using `page.wait_for_download()`.
29///
30/// # Example
31///
32/// ```ignore
33/// let download = page.wait_for_download(async {
34///     page.locator("a.download").click().await?;
35///     Ok(())
36/// }).await?;
37///
38/// // Get the downloaded file path
39/// let path = download.path().await?;
40/// println!("Downloaded to: {}", path.display());
41///
42/// // Or save to a custom location
43/// download.save_as("./downloads/my-file.pdf").await?;
44/// ```
45#[derive(Debug)]
46pub struct Download {
47    /// Global unique identifier.
48    guid: String,
49    /// Download URL.
50    url: String,
51    /// Suggested filename from the browser.
52    suggested_filename: String,
53    /// Temporary file path where the download is saved.
54    temp_path: Option<PathBuf>,
55    /// State of the download.
56    state: DownloadState,
57    /// Failure reason if any.
58    failure: Option<String>,
59    /// Receiver for state updates.
60    state_rx: watch::Receiver<DownloadState>,
61    /// Receiver for path updates.
62    path_rx: watch::Receiver<Option<PathBuf>>,
63}
64
65impl Download {
66    /// Create a new Download.
67    pub(crate) fn new(
68        guid: String,
69        url: String,
70        suggested_filename: String,
71        state_rx: watch::Receiver<DownloadState>,
72        path_rx: watch::Receiver<Option<PathBuf>>,
73    ) -> Self {
74        Self {
75            guid,
76            url,
77            suggested_filename,
78            temp_path: None,
79            state: DownloadState::InProgress,
80            failure: None,
81            state_rx,
82            path_rx,
83        }
84    }
85
86    /// Get the download URL.
87    pub fn url(&self) -> &str {
88        &self.url
89    }
90
91    /// Get the suggested filename from the browser.
92    ///
93    /// This is the filename that the browser suggested based on the
94    /// Content-Disposition header or URL.
95    pub fn suggested_filename(&self) -> &str {
96        &self.suggested_filename
97    }
98
99    /// Get the global unique identifier of this download.
100    pub fn guid(&self) -> &str {
101        &self.guid
102    }
103
104    /// Get the path to the downloaded file.
105    ///
106    /// This method waits for the download to complete if it's still in progress.
107    /// The file is saved to a temporary location by default.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the download fails or is canceled.
112    #[instrument(level = "debug", skip(self), fields(guid = %self.guid))]
113    pub async fn path(&mut self) -> Result<PathBuf, NetworkError> {
114        // Wait for the path to be available
115        let mut path_rx = self.path_rx.clone();
116        
117        loop {
118            {
119                let path = path_rx.borrow();
120                if let Some(ref p) = *path {
121                    self.temp_path = Some(p.clone());
122                    return Ok(p.clone());
123                }
124            }
125            
126            // Check if we've failed
127            if self.failure.is_some() {
128                return Err(NetworkError::IoError(
129                    self.failure.clone().unwrap_or_else(|| "Unknown download error".to_string()),
130                ));
131            }
132
133            // Wait for changes
134            if path_rx.changed().await.is_err() {
135                return Err(NetworkError::Aborted);
136            }
137        }
138    }
139
140    /// Save the downloaded file to a custom location.
141    ///
142    /// This method waits for the download to complete if it's still in progress,
143    /// then copies the file to the specified path.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if:
148    /// - The download fails or is canceled
149    /// - The file cannot be copied to the destination
150    #[instrument(level = "debug", skip(self), fields(guid = %self.guid, dest = %dest.as_ref().display()))]
151    pub async fn save_as(&mut self, dest: impl AsRef<Path>) -> Result<(), NetworkError> {
152        let source = self.path().await?;
153        
154        debug!("Copying download to destination");
155        tokio::fs::copy(&source, dest.as_ref())
156            .await
157            .map_err(|e| NetworkError::IoError(e.to_string()))?;
158        
159        Ok(())
160    }
161
162    /// Cancel the download.
163    ///
164    /// This method cancels an in-progress download. If the download has already
165    /// completed, this has no effect.
166    #[instrument(level = "debug", skip(self), fields(guid = %self.guid))]
167    pub async fn cancel(&mut self) -> Result<(), NetworkError> {
168        // For now, just mark it as canceled
169        // In a full implementation, we'd send a CDP command to cancel
170        self.state = DownloadState::Canceled;
171        self.failure = Some("canceled".to_string());
172        Ok(())
173    }
174
175    /// Get the failure reason if the download failed.
176    ///
177    /// Returns `None` if the download completed successfully or is still in progress.
178    pub fn failure(&self) -> Option<&str> {
179        self.failure.as_deref()
180    }
181
182    /// Update the download state.
183    pub(crate) fn update_state(&mut self, state: DownloadState, failure: Option<String>) {
184        self.state = state;
185        if let Some(f) = failure {
186            self.failure = Some(f);
187        }
188    }
189
190    /// Set the temporary path.
191    pub(crate) fn set_path(&mut self, path: PathBuf) {
192        self.temp_path = Some(path);
193    }
194}
195
196/// Manager for tracking downloads.
197#[derive(Debug)]
198pub(crate) struct DownloadManager {
199    /// Base download directory.
200    download_dir: PathBuf,
201}
202
203impl DownloadManager {
204    /// Create a new download manager.
205    pub fn new() -> Self {
206        // Use temp directory by default
207        let download_dir = std::env::temp_dir().join("viewpoint-downloads");
208        Self { download_dir }
209    }
210
211    /// Get the download directory.
212    pub fn download_dir(&self) -> &Path {
213        &self.download_dir
214    }
215}
216
217impl Default for DownloadManager {
218    fn default() -> Self {
219        Self::new()
220    }
221}