Skip to main content

soar_dl/
release.rs

1use std::{path::PathBuf, sync::Arc};
2
3use crate::{
4    download::Download,
5    error::DownloadError,
6    filter::Filter,
7    traits::{Asset as _, Platform, Release as _},
8    types::{OverwriteMode, Progress},
9};
10
11pub struct ReleaseDownload<P: Platform> {
12    project: String,
13    tag: Option<String>,
14    filter: Filter,
15    output: Option<String>,
16    overwrite: OverwriteMode,
17    extract: bool,
18    extract_to: Option<PathBuf>,
19    on_progress: Option<Arc<dyn Fn(Progress) + Send + Sync>>,
20    _platform: std::marker::PhantomData<P>,
21}
22
23impl<P: Platform> ReleaseDownload<P> {
24    /// Creates a new `ReleaseDownload` configured for the given project with sensible defaults.
25    ///
26    /// The returned builder is initialized with:
27    /// - `tag = None`
28    /// - a default `Filter`
29    /// - no explicit output path
30    /// - `overwrite = OverwriteMode::Prompt`
31    /// - extraction disabled
32    /// - no extraction path
33    /// - no progress callback
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use soar_dl::release::ReleaseDownload;
39    /// use soar_dl::github::Github;
40    ///
41    /// let dl = ReleaseDownload::<Github>::new("owner/repo");
42    /// // You can then chain further configuration:
43    /// // let dl = dl.tag("v1.2.3").output("downloads/").extract(true);
44    /// ```
45    pub fn new(project: impl Into<String>) -> Self {
46        Self {
47            project: project.into(),
48            tag: None,
49            filter: Filter::default(),
50            output: None,
51            overwrite: OverwriteMode::Prompt,
52            extract: false,
53            extract_to: None,
54            on_progress: None,
55            _platform: std::marker::PhantomData,
56        }
57    }
58
59    /// Sets the release tag to target when selecting a release.
60    ///
61    /// The provided tag will be used by `execute` to find a release with a matching tag.
62    /// Returns the updated builder to allow method chaining.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use soar_dl::release::ReleaseDownload;
68    /// use soar_dl::github::Github;
69    ///
70    /// let builder = ReleaseDownload::<Github>::new("owner/repo").tag("v1.2.3");
71    /// ```
72    pub fn tag(mut self, tag: impl Into<String>) -> Self {
73        self.tag = Some(tag.into());
74        self
75    }
76
77    /// Sets the asset filter used to select which release assets will be downloaded.
78    ///
79    /// The provided `filter` will be used to match asset names when executing the download.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use soar_dl::release::ReleaseDownload;
85    /// use soar_dl::filter::Filter;
86    /// use soar_dl::github::Github;
87    ///
88    /// let _rd = ReleaseDownload::<Github>::new("owner/repo").filter(Filter::default());
89    /// ```
90    pub fn filter(mut self, filter: Filter) -> Self {
91        self.filter = filter;
92        self
93    }
94
95    /// Sets the base output path for downloaded assets.
96    ///
97    /// The provided path will be used as the destination directory or base file path when downloads are written.
98    ///
99    /// # Returns
100    ///
101    /// The modified `ReleaseDownload` builder with the output path set.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use soar_dl::release::ReleaseDownload;
107    /// use soar_dl::github::Github;
108    ///
109    /// let dl = ReleaseDownload::<Github>::new("owner/repo").output("downloads");
110    /// ```
111    pub fn output(mut self, path: impl Into<String>) -> Self {
112        self.output = Some(path.into());
113        self
114    }
115
116    /// Set the overwrite behavior for downloaded files.
117    ///
118    /// `mode` determines how existing files are handled when downloading (for example, overwrite, skip, or prompt).
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use soar_dl::release::ReleaseDownload;
124    /// use soar_dl::types::OverwriteMode;
125    /// use soar_dl::github::Github;
126    ///
127    /// let dl = ReleaseDownload::<Github>::new("owner/repo").overwrite(OverwriteMode::Force);
128    /// ```
129    pub fn overwrite(mut self, mode: OverwriteMode) -> Self {
130        self.overwrite = mode;
131        self
132    }
133
134    /// Enables or disables extraction of downloaded assets.
135    ///
136    /// When set to `true`, assets that are archives will be extracted after they are downloaded.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use soar_dl::release::ReleaseDownload;
142    /// use soar_dl::github::Github;
143    ///
144    /// let rd = ReleaseDownload::<Github>::new("owner/repo").extract(true);
145    /// ```
146    pub fn extract(mut self, extract: bool) -> Self {
147        self.extract = extract;
148        self
149    }
150
151    /// Sets the destination directory where downloaded archives will be extracted.
152    ///
153    /// # Arguments
154    ///
155    /// * `path` - Destination path to extract downloaded assets into.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use soar_dl::release::ReleaseDownload;
161    /// use soar_dl::github::Github;
162    ///
163    /// let rd = ReleaseDownload::<Github>::new("owner/repo").extract_to("out/artifacts");
164    /// ```
165    pub fn extract_to(mut self, path: impl Into<PathBuf>) -> Self {
166        self.extract_to = Some(path.into());
167        self
168    }
169
170    /// Registers a callback that will be invoked with progress updates for each download.
171    ///
172    /// The provided callback is stored and called with `Progress` events as assets are downloaded.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use std::sync::Arc;
178    /// use soar_dl::release::ReleaseDownload;
179    /// use soar_dl::types::Progress;
180    /// use soar_dl::github::Github;
181    ///
182    /// let _rd = ReleaseDownload::<Github>::new("owner/repo")
183    ///     .progress(|progress: Progress| {
184    ///         // handle progress (e.g., log or update UI)
185    ///         println!("{:?}", progress);
186    ///     });
187    /// ```
188    pub fn progress<F>(mut self, f: F) -> Self
189    where
190        F: Fn(Progress) + Send + Sync + 'static,
191    {
192        self.on_progress = Some(Arc::new(f));
193        self
194    }
195
196    /// Downloads matched assets for a project's release and returns their local file paths.
197    ///
198    /// Selects a release by the configured tag if provided; otherwise prefers the first non-prerelease
199    /// release or falls back to the first release.
200    ///
201    /// Filters the release's assets using the configured `Filter`, downloads each matching asset with the configured
202    /// output, overwrite, and extraction options, and returns a vector of the resulting local `PathBuf`s.
203    ///
204    /// Returns an error if no release is found or if no assets match the filter.
205    ///
206    /// # Returns
207    ///
208    /// A `Vec<PathBuf>` containing the local paths of the downloaded assets on success, or a
209    /// `DownloadError` on failure.
210    ///
211    /// # Examples
212    ///
213    /// ```no_run
214    /// use std::path::PathBuf;
215    /// use soar_dl::release::ReleaseDownload;
216    /// use soar_dl::github::Github;
217    /// use soar_dl::filter::Filter;
218    ///
219    /// let paths: Vec<PathBuf> = ReleaseDownload::<Github>::new("owner/repo")
220    ///     .tag("v1.0")
221    ///     .filter(Filter::default())
222    ///     .output("downloads")
223    ///     .execute()
224    ///     .unwrap();
225    ///
226    /// assert!(!paths.is_empty());
227    /// ```
228    pub fn execute(self) -> Result<Vec<PathBuf>, DownloadError> {
229        let releases = P::fetch_releases(&self.project, self.tag.as_deref())?;
230
231        let release = if let Some(ref tag) = self.tag {
232            releases.iter().find(|r| r.tag() == tag)
233        } else {
234            releases
235                .iter()
236                .find(|r| !r.is_prerelease())
237                .or_else(|| releases.first())
238        };
239
240        let release = release.ok_or_else(|| DownloadError::InvalidResponse)?;
241
242        let assets: Vec<_> = release
243            .assets()
244            .iter()
245            .filter(|a| self.filter.matches(a.name()))
246            .collect();
247
248        if assets.is_empty() {
249            return Err(DownloadError::NoMatch {
250                available: release
251                    .assets()
252                    .iter()
253                    .map(|a| a.name().to_string())
254                    .collect(),
255            });
256        }
257
258        let mut paths = Vec::new();
259        for asset in assets {
260            let mut dl = Download::new(asset.url())
261                .overwrite(self.overwrite)
262                .extract(self.extract);
263
264            if let Some(ref output) = self.output {
265                dl = dl.output(output);
266            }
267
268            if let Some(ref extract_to) = self.extract_to {
269                dl = dl.extract_to(extract_to);
270            }
271
272            if let Some(ref cb) = self.on_progress {
273                let cb = cb.clone();
274                dl = dl.progress(move |p| cb(p));
275            }
276
277            let path = dl.execute()?;
278
279            paths.push(path);
280        }
281
282        Ok(paths)
283    }
284}