playwright_cdp/
download.rs1use crate::error::{Error, Result};
9use crate::page::Page;
10use parking_lot::Mutex;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use std::time::Duration;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
18pub(crate) enum DownloadState {
19 InProgress,
20 Completed,
21 Canceled,
22}
23
24#[derive(Clone)]
25pub(crate) struct DownloadStateCell {
26 pub state: Arc<Mutex<DownloadState>>,
27}
28
29impl DownloadStateCell {
30 pub(crate) fn new() -> Self {
31 Self {
32 state: Arc::new(Mutex::new(DownloadState::InProgress)),
33 }
34 }
35}
36
37impl Default for DownloadStateCell {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43#[derive(Clone)]
49pub struct Download {
50 inner: Arc<DownloadInner>,
51}
52
53struct DownloadInner {
54 url: String,
55 suggested_filename: String,
56 #[allow(dead_code)]
57 guid: String,
58 state: DownloadStateCell,
59 download_path: PathBuf,
61 page: Page,
62}
63
64impl Download {
65 pub(crate) fn new(
66 url: String,
67 suggested_filename: String,
68 guid: String,
69 state: DownloadStateCell,
70 download_path: PathBuf,
71 page: Page,
72 ) -> Self {
73 Self {
74 inner: Arc::new(DownloadInner {
75 url,
76 suggested_filename,
77 guid,
78 state,
79 download_path,
80 page,
81 }),
82 }
83 }
84
85 pub fn url(&self) -> &str {
87 &self.inner.url
88 }
89
90 pub fn page(&self) -> Page {
95 self.inner.page.clone()
96 }
97
98 pub fn suggested_filename(&self) -> &str {
100 &self.inner.suggested_filename
101 }
102
103 pub fn failure(&self) -> Option<&str> {
105 if *self.inner.state.state.lock() == DownloadState::Canceled {
106 Some("download canceled")
107 } else {
108 None
109 }
110 }
111
112 async fn await_completion(&self) -> Result<()> {
118 let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
119 loop {
120 match *self.inner.state.state.lock() {
121 DownloadState::Completed => return Ok(()),
122 DownloadState::Canceled => {
123 return Err(Error::ProtocolError("download was canceled".into()))
124 }
125 DownloadState::InProgress => {}
126 }
127 if let Some(p) = self.find_stable_file().await {
129 if p.extension().and_then(|e| e.to_str()) == Some("crdownload") {
132 let final_path = self
133 .inner
134 .download_path
135 .join(&self.inner.suggested_filename);
136 let _ = tokio::fs::rename(&p, &final_path).await;
137 }
138 return Ok(());
139 }
140 if tokio::time::Instant::now() >= deadline {
141 return Err(Error::Timeout(
142 "timed out waiting for download to complete".into(),
143 ));
144 }
145 tokio::time::sleep(Duration::from_millis(50)).await;
146 }
147 }
148
149 async fn find_stable_file(&self) -> Option<PathBuf> {
152 let final_path = self.inner.download_path.join(&self.inner.suggested_filename);
153 if self.size_stable(&final_path).await {
154 return Some(final_path);
155 }
156 let mut entries = tokio::fs::read_dir(&self.inner.download_path).await.ok()?;
158 while let Ok(Some(e)) = entries.next_entry().await {
159 let p = e.path();
160 if p.extension().and_then(|x| x.to_str()) == Some("crdownload")
161 && self.size_stable(&p).await
162 {
163 return Some(p);
164 }
165 }
166 None
167 }
168
169 async fn size_stable(&self, path: &Path) -> bool {
171 let s1 = tokio::fs::metadata(path).await.map(|m| m.len()).ok();
172 tokio::time::sleep(Duration::from_millis(150)).await;
173 let s2 = tokio::fs::metadata(path).await.map(|m| m.len()).ok();
174 s1.is_some() && s1 == s2
175 }
176
177 pub async fn path(&self) -> Result<Option<PathBuf>> {
182 if self.await_completion().await.is_err() {
183 return Ok(None);
184 }
185 let target = self.inner.download_path.join(&self.inner.suggested_filename);
186 let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
189 loop {
190 if tokio::fs::metadata(&target).await.is_ok() {
191 return Ok(Some(target));
192 }
193 if tokio::time::Instant::now() >= deadline {
194 return Ok(Some(target)); }
196 tokio::time::sleep(Duration::from_millis(50)).await;
197 }
198 }
199
200 pub async fn save_as(&self, to: impl AsRef<Path>) -> Result<PathBuf> {
202 self.await_completion().await?;
203 let from = self.path().await?.ok_or_else(|| {
204 Error::ProtocolError("download has no file to save (failed)".into())
205 })?;
206 let to = to.as_ref().to_path_buf();
207 if let Some(parent) = to.parent() {
208 if !parent.as_os_str().is_empty() {
209 tokio::fs::create_dir_all(parent).await?;
210 }
211 }
212 tokio::fs::copy(&from, &to).await?;
213 Ok(to)
214 }
215
216 pub async fn delete(&self) -> Result<()> {
218 let target = self.inner.download_path.join(&self.inner.suggested_filename);
219 match tokio::fs::remove_file(&target).await {
220 Ok(()) => Ok(()),
221 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
222 Err(e) => Err(Error::from(e)),
223 }
224 }
225
226 pub async fn create_read_stream(&self) -> Result<tokio::fs::File> {
228 self.await_completion().await?;
229 let from = self.path().await?.ok_or_else(|| {
230 Error::ProtocolError("download has no file to read (failed)".into())
231 })?;
232 Ok(tokio::fs::File::open(from).await?)
233 }
234}