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}