browser_commander/downloads/options.rs
1//! What a managed download is, and what can be asked of one (issue #88).
2//!
3//! These are the types callers name: the lifecycle states, the record of one
4//! download, and the two option builders. They are kept apart from
5//! [`crate::downloads::manager`] because the vocabulary is what a caller reads,
6//! while the manager is what the browser drives.
7//!
8//! # Example
9//!
10//! ```rust
11//! use std::time::Duration;
12//!
13//! use browser_commander::downloads::{CaptureOptions, DownloadOptions};
14//!
15//! let browser_wide = DownloadOptions::default().directory("/tmp/reports");
16//! let one_download = CaptureOptions::named("q3.pdf").within(Duration::from_secs(20));
17//! assert_eq!(browser_wide.directory.as_deref(), Some("/tmp/reports"));
18//! assert_eq!(one_download.timeout, Some(Duration::from_secs(20)));
19//! ```
20
21use std::fmt;
22use std::path::PathBuf;
23use std::sync::Arc;
24use std::time::Duration;
25
26use crate::downloads::store::{DownloadConflict, DownloadNamer, DownloadValidator};
27
28/// Lifecycle states every download passes through.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum DownloadEvent {
32 /// The engine announced a download.
33 Started,
34 /// The bytes are on disk under their final name.
35 Completed,
36 /// The download ended in an error.
37 Failed,
38 /// The browser or the person stopped it.
39 Cancelled,
40}
41
42impl DownloadEvent {
43 /// The name this state is known by in every language the library ships in.
44 pub fn as_str(&self) -> &'static str {
45 match self {
46 Self::Started => "started",
47 Self::Completed => "completed",
48 Self::Failed => "failed",
49 Self::Cancelled => "cancelled",
50 }
51 }
52}
53
54impl fmt::Display for DownloadEvent {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str(self.as_str())
57 }
58}
59
60/// Everything known about one download.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct DownloadArtifact {
63 /// Identifier this download is reported under.
64 pub id: String,
65 /// Source URL, when the engine reports one.
66 pub url: Option<String>,
67 /// Name the page suggested.
68 pub suggested_filename: Option<String>,
69 /// Where the download is in its lifecycle.
70 pub state: DownloadEvent,
71 /// When the engine announced it, as an ISO-8601 UTC timestamp.
72 pub started_at: String,
73 /// When it settled, as an ISO-8601 UTC timestamp.
74 pub completed_at: Option<String>,
75 /// Final path, once the bytes have been placed.
76 pub path: Option<PathBuf>,
77 /// MIME type declared by the server, when there is one.
78 pub mime_type: Option<String>,
79 /// Size in bytes, once the bytes have been placed.
80 pub bytes: Option<u64>,
81 /// Hex-encoded SHA-256 of the saved bytes.
82 pub checksum: Option<String>,
83 /// Whatever the engine or the store reported, kept verbatim.
84 pub failure: Option<String>,
85}
86
87/// Naming, validation and conflict rules one [`DownloadManager::capture`] owns.
88#[derive(Clone, Default)]
89pub struct CaptureOptions {
90 /// Name for this download, overriding what the page suggested.
91 pub filename: Option<DownloadNamer>,
92 /// Budget for the whole capture.
93 pub timeout: Option<Duration>,
94 /// Validation for this download.
95 pub validate: Option<DownloadValidator>,
96 /// Conflict policy for this download.
97 pub conflict: Option<DownloadConflict>,
98}
99
100impl CaptureOptions {
101 /// Capture the next download under a fixed name.
102 ///
103 /// # Arguments
104 ///
105 /// * `name` - Name to save the download as
106 ///
107 /// # Returns
108 ///
109 /// Options naming the download the caller is about to trigger.
110 pub fn named(name: impl Into<String>) -> Self {
111 let name = name.into();
112 Self {
113 filename: Some(Arc::new(move |_naming| name.clone())),
114 ..Self::default()
115 }
116 }
117
118 /// Give the whole capture a different budget.
119 pub fn within(mut self, timeout: Duration) -> Self {
120 self.timeout = Some(timeout);
121 self
122 }
123
124 /// Check this download before it is published.
125 pub fn validated_by(mut self, validate: DownloadValidator) -> Self {
126 self.validate = Some(validate);
127 self
128 }
129
130 /// Resolve a name collision differently for this download.
131 pub fn on_conflict(mut self, conflict: DownloadConflict) -> Self {
132 self.conflict = Some(conflict);
133 self
134 }
135}
136
137impl fmt::Debug for CaptureOptions {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.debug_struct("CaptureOptions")
140 .field("filename", &self.filename.is_some())
141 .field("timeout", &self.timeout)
142 .field("validate", &self.validate.is_some())
143 .field("conflict", &self.conflict)
144 .finish()
145 }
146}
147
148/// How a browser's downloads are managed.
149#[derive(Clone)]
150pub struct DownloadOptions {
151 /// Absolute path, [`DownloadDirectoryPreset::USER_DOWNLOADS`] or
152 /// [`DownloadDirectoryPreset::TEMPORARY`].
153 ///
154 /// [`DownloadDirectoryPreset::USER_DOWNLOADS`]: crate::downloads::DownloadDirectoryPreset::USER_DOWNLOADS
155 /// [`DownloadDirectoryPreset::TEMPORARY`]: crate::downloads::DownloadDirectoryPreset::TEMPORARY
156 pub directory: Option<String>,
157 /// Keep files after the browser closes. Always true today, and named so
158 /// that a future non-persistent mode cannot change this one silently.
159 pub persist: bool,
160 /// How a name that is already taken is resolved.
161 pub conflict: DownloadConflict,
162 /// Naming callback for every download.
163 pub filename: Option<DownloadNamer>,
164 /// Validation for every download.
165 pub validate: Option<DownloadValidator>,
166 /// How often the staging directory is listed.
167 pub poll_interval: Option<Duration>,
168}
169
170impl Default for DownloadOptions {
171 fn default() -> Self {
172 Self {
173 directory: None,
174 persist: true,
175 conflict: DownloadConflict::default(),
176 filename: None,
177 validate: None,
178 poll_interval: None,
179 }
180 }
181}
182
183impl DownloadOptions {
184 /// Save downloads into a specific directory or preset.
185 pub fn directory(mut self, directory: impl Into<String>) -> Self {
186 self.directory = Some(directory.into());
187 self
188 }
189
190 /// Resolve name collisions with this policy.
191 pub fn conflict(mut self, conflict: DownloadConflict) -> Self {
192 self.conflict = conflict;
193 self
194 }
195
196 /// Name every download with this callback.
197 pub fn filename(mut self, filename: DownloadNamer) -> Self {
198 self.filename = Some(filename);
199 self
200 }
201
202 /// Check every download before it is published.
203 pub fn validate(mut self, validate: DownloadValidator) -> Self {
204 self.validate = Some(validate);
205 self
206 }
207
208 /// List the staging directory this often.
209 pub fn poll_interval(mut self, interval: Duration) -> Self {
210 self.poll_interval = Some(interval);
211 self
212 }
213}
214
215impl fmt::Debug for DownloadOptions {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 f.debug_struct("DownloadOptions")
218 .field("directory", &self.directory)
219 .field("persist", &self.persist)
220 .field("conflict", &self.conflict)
221 .field("filename", &self.filename.is_some())
222 .field("validate", &self.validate.is_some())
223 .field("poll_interval", &self.poll_interval)
224 .finish()
225 }
226}