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
88/// [`DownloadManager::capture`](crate::downloads::DownloadManager::capture) owns.
89#[derive(Clone, Default)]
90pub struct CaptureOptions {
91 /// Name for this download, overriding what the page suggested.
92 pub filename: Option<DownloadNamer>,
93 /// Budget for the whole capture.
94 pub timeout: Option<Duration>,
95 /// Validation for this download.
96 pub validate: Option<DownloadValidator>,
97 /// Conflict policy for this download.
98 pub conflict: Option<DownloadConflict>,
99}
100
101impl CaptureOptions {
102 /// Capture the next download under a fixed name.
103 ///
104 /// # Arguments
105 ///
106 /// * `name` - Name to save the download as
107 ///
108 /// # Returns
109 ///
110 /// Options naming the download the caller is about to trigger.
111 pub fn named(name: impl Into<String>) -> Self {
112 let name = name.into();
113 Self {
114 filename: Some(Arc::new(move |_naming| name.clone())),
115 ..Self::default()
116 }
117 }
118
119 /// Give the whole capture a different budget.
120 pub fn within(mut self, timeout: Duration) -> Self {
121 self.timeout = Some(timeout);
122 self
123 }
124
125 /// Check this download before it is published.
126 pub fn validated_by(mut self, validate: DownloadValidator) -> Self {
127 self.validate = Some(validate);
128 self
129 }
130
131 /// Resolve a name collision differently for this download.
132 pub fn on_conflict(mut self, conflict: DownloadConflict) -> Self {
133 self.conflict = Some(conflict);
134 self
135 }
136}
137
138impl fmt::Debug for CaptureOptions {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 f.debug_struct("CaptureOptions")
141 .field("filename", &self.filename.is_some())
142 .field("timeout", &self.timeout)
143 .field("validate", &self.validate.is_some())
144 .field("conflict", &self.conflict)
145 .finish()
146 }
147}
148
149/// How a browser's downloads are managed.
150#[derive(Clone)]
151pub struct DownloadOptions {
152 /// Absolute path, [`DownloadDirectoryPreset::USER_DOWNLOADS`] or
153 /// [`DownloadDirectoryPreset::TEMPORARY`].
154 ///
155 /// [`DownloadDirectoryPreset::USER_DOWNLOADS`]: crate::downloads::DownloadDirectoryPreset::USER_DOWNLOADS
156 /// [`DownloadDirectoryPreset::TEMPORARY`]: crate::downloads::DownloadDirectoryPreset::TEMPORARY
157 pub directory: Option<String>,
158 /// Keep files after the browser closes. Always true today, and named so
159 /// that a future non-persistent mode cannot change this one silently.
160 pub persist: bool,
161 /// How a name that is already taken is resolved.
162 pub conflict: DownloadConflict,
163 /// Naming callback for every download.
164 pub filename: Option<DownloadNamer>,
165 /// Validation for every download.
166 pub validate: Option<DownloadValidator>,
167 /// How often the staging directory is listed.
168 pub poll_interval: Option<Duration>,
169}
170
171impl Default for DownloadOptions {
172 fn default() -> Self {
173 Self {
174 directory: None,
175 persist: true,
176 conflict: DownloadConflict::default(),
177 filename: None,
178 validate: None,
179 poll_interval: None,
180 }
181 }
182}
183
184impl DownloadOptions {
185 /// Save downloads into a specific directory or preset.
186 pub fn directory(mut self, directory: impl Into<String>) -> Self {
187 self.directory = Some(directory.into());
188 self
189 }
190
191 /// Resolve name collisions with this policy.
192 pub fn conflict(mut self, conflict: DownloadConflict) -> Self {
193 self.conflict = conflict;
194 self
195 }
196
197 /// Name every download with this callback.
198 pub fn filename(mut self, filename: DownloadNamer) -> Self {
199 self.filename = Some(filename);
200 self
201 }
202
203 /// Check every download before it is published.
204 pub fn validate(mut self, validate: DownloadValidator) -> Self {
205 self.validate = Some(validate);
206 self
207 }
208
209 /// List the staging directory this often.
210 pub fn poll_interval(mut self, interval: Duration) -> Self {
211 self.poll_interval = Some(interval);
212 self
213 }
214}
215
216impl fmt::Debug for DownloadOptions {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 f.debug_struct("DownloadOptions")
219 .field("directory", &self.directory)
220 .field("persist", &self.persist)
221 .field("conflict", &self.conflict)
222 .field("filename", &self.filename.is_some())
223 .field("validate", &self.validate.is_some())
224 .field("poll_interval", &self.poll_interval)
225 .finish()
226 }
227}