Skip to main content

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}