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 [`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}