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}