glass/browser/session/
download.rs1use super::*;
8
9impl BrowserSession {
10 pub async fn wait_for_download(
12 &self,
13 destination: &Path,
14 deadline: Duration,
15 ) -> BrowserResult<DownloadOutcome> {
16 self.policy.require(PolicyCapability::Download)?;
17 if deadline.is_zero() || deadline > MAX_DIAGNOSTIC_DURATION {
18 return Err("download deadline must be between 1 ms and 30 seconds".into());
19 }
20 let destination = self.policy.require_existing_path(destination)?;
21 if !destination.is_dir() || !destination.starts_with(&self.upload_root) {
22 return Err(
23 "download destination must be a directory inside the authorized root".into(),
24 );
25 }
26 let (target_id, frame_id) = self.route_identity().await?;
27 let page_session_id = if use_page_download_compatibility(
28 self.chrome.is_some(),
29 self.disposable_profile.is_some(),
30 ) {
31 let topology = self.topology.lock().await;
32 if topology.active_target_id.as_deref() != Some(target_id.as_str()) {
33 return Err(download_error(
34 DownloadErrorKind::AuthorizationFailed,
35 "incognito download route changed during capture",
36 )
37 .into());
38 }
39 Some(topology.active_target_session_id.clone().ok_or_else(|| {
40 download_error(
41 DownloadErrorKind::AuthorizationFailed,
42 "incognito download has no captured top-level page session",
43 )
44 })?)
45 } else {
46 None
47 };
48 let _download_scope = self.download_scope.lock().await;
49 let mut events = self.cdp.subscribe_events_with_params();
50 let mut download_guard = match page_session_id {
51 Some(page_session_id) => {
52 DownloadBehaviorGuard::acquire_for_incognito(
53 self.cdp.clone(),
54 destination.clone(),
55 target_id.clone(),
56 page_session_id,
57 self.launched_incognito_context_id.clone().ok_or_else(|| {
58 download_error(
59 DownloadErrorKind::AuthorizationFailed,
60 "owned incognito session has no original browser context ID",
61 )
62 })?,
63 )
64 .await?
65 }
66 None => {
67 DownloadBehaviorGuard::acquire(self.cdp.clone(), destination.clone(), None).await?
68 }
69 };
70 let result = tokio::time::timeout(deadline, async {
71 let mut guid = None;
72 let mut filename = String::new();
73 loop {
74 match events.recv().await {
75 Ok(event) if event.method == "Browser.downloadWillBegin" => {
76 if event.params["frameId"].as_str() != Some(frame_id.as_str()) {
77 continue;
78 }
79 guid = event.params["guid"].as_str().map(bounded_diagnostic_text);
80 filename = bounded_diagnostic_text(
81 event.params["suggestedFilename"]
82 .as_str()
83 .unwrap_or("download"),
84 );
85 }
86 Ok(event) if event.method == "Browser.downloadProgress" => {
87 let Some(active_guid) = guid.as_deref() else {
88 continue;
89 };
90 if event.params["guid"].as_str() != Some(active_guid) {
91 continue;
92 }
93 let state = event.params["state"].as_str().unwrap_or("inProgress");
94 if matches!(state, "completed" | "canceled") {
95 self.record_audit(
96 "download",
97 format!("{} (state={})", filename, state),
98 );
99 return BrowserResult::Ok(DownloadOutcome {
100 guid: active_guid.to_string(),
101 suggested_filename: filename,
102 state: state.to_ascii_lowercase(),
103 received_bytes: finite_nonnegative_u64(
104 &event.params["receivedBytes"],
105 ),
106 total_bytes: finite_nonnegative_u64(&event.params["totalBytes"]),
107 target_id: target_id.clone(),
108 frame_id: frame_id.clone(),
109 sha256: None,
110 });
111 }
112 }
113 Ok(_) => {}
114 Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
115 return Err(format!("download event stream dropped {count} events").into());
116 }
117 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
118 return Err("download event stream closed".into());
119 }
120 }
121 }
122 })
123 .await
124 .unwrap_or_else(|_| Err("download deadline exceeded".into()));
125 download_guard.disable().await?;
126 result
127 }
128}