1use std::{
2 io::{self, Cursor, Read},
3 path::PathBuf,
4 string::FromUtf8Error,
5};
6
7use bon::Builder;
8use bytes::Bytes;
9use thiserror::Error;
10use url::{ParseError, Url};
11
12use crate::{
13 config::Config,
14 git::GitSource,
15 lockfile::RemotePackageSourceUrl,
16 lua_rockspec::{LuaRockspecError, RemoteLuaRockspec, RockSourceSpec},
17 luarocks,
18 package::{
19 PackageName, PackageReq, PackageSpec, PackageSpecFromPackageReqError, PackageVersion,
20 RemotePackageTypeFilterSpec,
21 },
22 progress::{Progress, ProgressBar},
23 remote_package_db::{RemotePackageDB, RemotePackageDBError, SearchError},
24 remote_package_source::RemotePackageSource,
25 rockspec::Rockspec,
26};
27
28pub struct Download<'a> {
30 package_req: &'a PackageReq,
31 package_db: Option<&'a RemotePackageDB>,
32 config: &'a Config,
33 progress: &'a Progress<ProgressBar>,
34}
35
36impl<'a> Download<'a> {
37 pub fn new(
39 package_req: &'a PackageReq,
40 config: &'a Config,
41 progress: &'a Progress<ProgressBar>,
42 ) -> Self {
43 Self {
44 package_req,
45 package_db: None,
46 config,
47 progress,
48 }
49 }
50
51 pub fn package_db(self, package_db: &'a RemotePackageDB) -> Self {
54 Self {
55 package_db: Some(package_db),
56 ..self
57 }
58 }
59
60 pub async fn download_rockspec(self) -> Result<DownloadedRockspec, SearchAndDownloadError> {
62 match self.package_db {
63 Some(db) => download_rockspec(self.package_req, db, self.progress).await,
64 None => {
65 let db = RemotePackageDB::from_config(self.config, self.progress).await?;
66 download_rockspec(self.package_req, &db, self.progress).await
67 }
68 }
69 }
70
71 pub async fn download_src_rock_to_file(
74 self,
75 destination_dir: Option<PathBuf>,
76 ) -> Result<DownloadedPackedRock, SearchAndDownloadError> {
77 match self.package_db {
78 Some(db) => {
79 download_src_rock_to_file(self.package_req, destination_dir, db, self.progress)
80 .await
81 }
82 None => {
83 let db = RemotePackageDB::from_config(self.config, self.progress).await?;
84 download_src_rock_to_file(self.package_req, destination_dir, &db, self.progress)
85 .await
86 }
87 }
88 }
89
90 pub async fn search_and_download_src_rock(
92 self,
93 ) -> Result<DownloadedPackedRockBytes, SearchAndDownloadError> {
94 match self.package_db {
95 Some(db) => search_and_download_src_rock(self.package_req, db, self.progress).await,
96 None => {
97 let db = RemotePackageDB::from_config(self.config, self.progress).await?;
98 search_and_download_src_rock(self.package_req, &db, self.progress).await
99 }
100 }
101 }
102
103 pub(crate) async fn download_remote_rock(
104 self,
105 ) -> Result<RemoteRockDownload, SearchAndDownloadError> {
106 match self.package_db {
107 Some(db) => download_remote_rock(self.package_req, db, self.progress).await,
108 None => {
109 let db = RemotePackageDB::from_config(self.config, self.progress).await?;
110 download_remote_rock(self.package_req, &db, self.progress).await
111 }
112 }
113 }
114}
115
116pub struct DownloadedPackedRockBytes {
117 pub name: PackageName,
118 pub version: PackageVersion,
119 pub bytes: Bytes,
120 pub file_name: String,
121 pub url: Url,
122}
123
124pub struct DownloadedPackedRock {
125 pub name: PackageName,
126 pub version: PackageVersion,
127 pub path: PathBuf,
128}
129
130#[derive(Clone, Debug)]
131pub struct DownloadedRockspec {
132 pub rockspec: RemoteLuaRockspec,
133 pub(crate) source: RemotePackageSource,
134 pub(crate) source_url: Option<RemotePackageSourceUrl>,
135}
136
137#[derive(Clone, Debug)]
138pub(crate) enum RemoteRockDownload {
139 RockspecOnly {
140 rockspec_download: DownloadedRockspec,
141 },
142 BinaryRock {
143 rockspec_download: DownloadedRockspec,
144 packed_rock: Bytes,
145 },
146 SrcRock {
147 rockspec_download: DownloadedRockspec,
148 src_rock: Bytes,
149 source_url: RemotePackageSourceUrl,
150 },
151}
152
153impl RemoteRockDownload {
154 pub fn rockspec(&self) -> &RemoteLuaRockspec {
155 &self.rockspec_download().rockspec
156 }
157 pub fn rockspec_download(&self) -> &DownloadedRockspec {
158 match self {
159 Self::RockspecOnly { rockspec_download }
160 | Self::BinaryRock {
161 rockspec_download, ..
162 }
163 | Self::SrcRock {
164 rockspec_download, ..
165 } => rockspec_download,
166 }
167 }
168 pub(crate) fn from_package_req_and_source_spec(
170 package_req: PackageReq,
171 source_spec: RockSourceSpec,
172 ) -> Result<Self, SearchAndDownloadError> {
173 let package_spec = package_req.try_into()?;
174 let source_url = Some(match &source_spec {
175 RockSourceSpec::Git(GitSource { url, checkout_ref }) => RemotePackageSourceUrl::Git {
176 url: url.to_string(),
177 checkout_ref: checkout_ref
178 .clone()
179 .ok_or(SearchAndDownloadError::MissingCheckoutRef(url.to_string()))?,
180 },
181 RockSourceSpec::File(path) => RemotePackageSourceUrl::File { path: path.clone() },
182 RockSourceSpec::Url(url) => RemotePackageSourceUrl::Url { url: url.clone() },
183 });
184 let rockspec = RemoteLuaRockspec::from_package_and_source_spec(package_spec, source_spec);
185 let rockspec_content = rockspec
186 .to_lua_remote_rockspec_string()
187 .expect("the infallible happened");
188 let rockspec_download = DownloadedRockspec {
189 rockspec,
190 source_url,
191 source: RemotePackageSource::RockspecContent(rockspec_content),
192 };
193 Ok(Self::RockspecOnly { rockspec_download })
194 }
195}
196
197#[derive(Error, Debug)]
198pub enum DownloadRockspecError {
199 #[error("failed to download rockspec: {0}")]
200 Request(#[from] reqwest::Error),
201 #[error("failed to convert rockspec response: {0}")]
202 ResponseConversion(#[from] FromUtf8Error),
203 #[error("error initialising remote package DB:\n{0}")]
204 RemotePackageDB(#[from] RemotePackageDBError),
205 #[error(transparent)]
206 DownloadSrcRock(#[from] DownloadSrcRockError),
207}
208
209async fn download_rockspec(
211 package_req: &PackageReq,
212 package_db: &RemotePackageDB,
213 progress: &Progress<ProgressBar>,
214) -> Result<DownloadedRockspec, SearchAndDownloadError> {
215 let rockspec = match download_remote_rock(package_req, package_db, progress).await? {
216 RemoteRockDownload::RockspecOnly {
217 rockspec_download: rockspec,
218 } => rockspec,
219 RemoteRockDownload::BinaryRock {
220 rockspec_download: rockspec,
221 ..
222 } => rockspec,
223 RemoteRockDownload::SrcRock {
224 rockspec_download: rockspec,
225 ..
226 } => rockspec,
227 };
228 Ok(rockspec)
229}
230
231async fn download_remote_rock(
232 package_req: &PackageReq,
233 package_db: &RemotePackageDB,
234 progress: &Progress<ProgressBar>,
235) -> Result<RemoteRockDownload, SearchAndDownloadError> {
236 let remote_package = package_db.find(package_req, None, progress)?;
237 progress.map(|p| p.set_message(format!("📥 Downloading rockspec for {package_req}")));
238 match &remote_package.source {
239 RemotePackageSource::LuarocksRockspec(url) => {
240 let package = &remote_package.package;
241 let rockspec_name = format!("{}-{}.rockspec", package.name(), package.version());
242 let bytes = reqwest::Client::new()
243 .get(format!("{}/{}", &url, rockspec_name))
244 .send()
245 .await
246 .map_err(DownloadRockspecError::Request)?
247 .error_for_status()
248 .map_err(DownloadRockspecError::Request)?
249 .bytes()
250 .await
251 .map_err(DownloadRockspecError::Request)?;
252 let content = String::from_utf8(bytes.into())?;
253 let rockspec = DownloadedRockspec {
254 rockspec: RemoteLuaRockspec::new(&content)?,
255 source: remote_package.source,
256 source_url: remote_package.source_url,
257 };
258 Ok(RemoteRockDownload::RockspecOnly {
259 rockspec_download: rockspec,
260 })
261 }
262 RemotePackageSource::RockspecContent(content) => {
263 let rockspec = DownloadedRockspec {
264 rockspec: RemoteLuaRockspec::new(content)?,
265 source: remote_package.source,
266 source_url: remote_package.source_url,
267 };
268 Ok(RemoteRockDownload::RockspecOnly {
269 rockspec_download: rockspec,
270 })
271 }
272 RemotePackageSource::LuarocksBinaryRock(url) => {
273 let url = if let Some(RemotePackageSourceUrl::Url { url }) = &remote_package.source_url
275 {
276 url
277 } else {
278 url
279 };
280 let rock = download_binary_rock(&remote_package.package, url, progress).await?;
281 let rockspec = DownloadedRockspec {
282 rockspec: unpack_rockspec(&rock).await?,
283 source: remote_package.source,
284 source_url: remote_package.source_url,
285 };
286 Ok(RemoteRockDownload::BinaryRock {
287 rockspec_download: rockspec,
288 packed_rock: rock.bytes,
289 })
290 }
291 RemotePackageSource::LuarocksSrcRock(url) => {
292 let url = if let Some(RemotePackageSourceUrl::Url { url }) = &remote_package.source_url
294 {
295 url.clone()
296 } else {
297 url.clone()
298 };
299 let rock = download_src_rock(&remote_package.package, &url, progress).await?;
300 let rockspec = DownloadedRockspec {
301 rockspec: unpack_rockspec(&rock).await?,
302 source: remote_package.source,
303 source_url: remote_package.source_url,
304 };
305 Ok(RemoteRockDownload::SrcRock {
306 rockspec_download: rockspec,
307 src_rock: rock.bytes,
308 source_url: RemotePackageSourceUrl::Url { url },
309 })
310 }
311 RemotePackageSource::Local => Err(SearchAndDownloadError::LocalSource),
312 #[cfg(test)]
313 RemotePackageSource::Test => unimplemented!(),
314 }
315}
316
317#[derive(Error, Debug)]
318pub enum SearchAndDownloadError {
319 #[error(transparent)]
320 Search(#[from] SearchError),
321 #[error(transparent)]
322 Download(#[from] DownloadSrcRockError),
323 #[error(transparent)]
324 DownloadRockspec(#[from] DownloadRockspecError),
325 #[error("io operation failed: {0}")]
326 Io(#[from] io::Error),
327 #[error("UTF-8 conversion failed: {0}")]
328 Utf8(#[from] FromUtf8Error),
329 #[error(transparent)]
330 Rockspec(#[from] LuaRockspecError),
331 #[error("error initialising remote package DB:\n{0}")]
332 RemotePackageDB(#[from] RemotePackageDBError),
333 #[error("failed to read packed rock {0}:\n{1}")]
334 ZipRead(String, zip::result::ZipError),
335 #[error("failed to extract packed rock {0}:\n{1}")]
336 ZipExtract(String, zip::result::ZipError),
337 #[error("{0} not found in the packed rock.")]
338 RockspecNotFoundInPackedRock(String),
339 #[error(transparent)]
340 PackageSpecFromPackageReq(#[from] PackageSpecFromPackageReqError),
341 #[error("git source {0} without a revision or tag.")]
342 MissingCheckoutRef(String),
343 #[error("cannot download from a local rock source.")]
344 LocalSource,
345 #[error("cannot download from a local rock or embedded rockspec source.")]
346 NonURLSource,
347}
348
349async fn search_and_download_src_rock(
350 package_req: &PackageReq,
351 package_db: &RemotePackageDB,
352 progress: &Progress<ProgressBar>,
353) -> Result<DownloadedPackedRockBytes, SearchAndDownloadError> {
354 let filter = Some(RemotePackageTypeFilterSpec {
355 rockspec: false,
356 binary: false,
357 src: true,
358 });
359 let remote_package = package_db.find(package_req, filter, progress)?;
360 let source_url = remote_package
361 .source
362 .url()
363 .ok_or(SearchAndDownloadError::NonURLSource)?;
364 Ok(download_src_rock(&remote_package.package, &source_url, progress).await?)
365}
366
367#[derive(Error, Debug)]
368pub enum DownloadSrcRockError {
369 #[error("failed to download source rock: {0}")]
370 Request(#[from] reqwest::Error),
371 #[error("failed to parse source rock URL: {0}")]
372 Parse(#[from] ParseError),
373}
374
375pub(crate) async fn download_src_rock(
376 package: &PackageSpec,
377 server_url: &Url,
378 progress: &Progress<ProgressBar>,
379) -> Result<DownloadedPackedRockBytes, DownloadSrcRockError> {
380 ArchiveDownload::new(package, server_url, "src.rock", progress)
381 .download()
382 .await
383}
384
385pub(crate) async fn download_binary_rock(
386 package: &PackageSpec,
387 server_url: &Url,
388 progress: &Progress<ProgressBar>,
389) -> Result<DownloadedPackedRockBytes, DownloadSrcRockError> {
390 let ext = format!("{}.rock", luarocks::current_platform_luarocks_identifier());
391 ArchiveDownload::new(package, server_url, &ext, progress)
392 .fallback_ext("all.rock")
393 .download()
394 .await
395}
396
397async fn download_src_rock_to_file(
398 package_req: &PackageReq,
399 destination_dir: Option<PathBuf>,
400 package_db: &RemotePackageDB,
401 progress: &Progress<ProgressBar>,
402) -> Result<DownloadedPackedRock, SearchAndDownloadError> {
403 progress.map(|p| p.set_message(format!("📥 Downloading {package_req}")));
404
405 let rock = search_and_download_src_rock(package_req, package_db, progress).await?;
406 let full_rock_name = mk_packed_rock_name(&rock.name, &rock.version, "src.rock");
407 tokio::fs::write(
408 destination_dir
409 .map(|dest| dest.join(&full_rock_name))
410 .unwrap_or_else(|| full_rock_name.clone().into()),
411 &rock.bytes,
412 )
413 .await?;
414
415 Ok(DownloadedPackedRock {
416 name: rock.name.to_owned(),
417 version: rock.version.to_owned(),
418 path: full_rock_name.into(),
419 })
420}
421
422#[derive(Builder)]
423#[builder(start_fn = new, finish_fn(name = _build, vis = ""))]
424struct ArchiveDownload<'a> {
425 #[builder(start_fn)]
426 package: &'a PackageSpec,
427
428 #[builder(start_fn)]
429 server_url: &'a Url,
430
431 #[builder(start_fn)]
432 ext: &'a str,
433
434 #[builder(start_fn)]
435 progress: &'a Progress<ProgressBar>,
436
437 fallback_ext: Option<&'a str>,
438}
439
440impl<State> ArchiveDownloadBuilder<'_, State>
441where
442 State: archive_download_builder::State,
443{
444 async fn download(self) -> Result<DownloadedPackedRockBytes, DownloadSrcRockError> {
445 let args = self._build();
446 let progress = args.progress;
447 let package = args.package;
448 let ext = args.ext;
449 let server_url = args.server_url;
450 progress.map(|p| {
451 p.set_message(format!(
452 "📥 Downloading {}-{}.{}",
453 package.name(),
454 package.version(),
455 ext,
456 ))
457 });
458 let full_rock_name = mk_packed_rock_name(package.name(), package.version(), ext);
459 let url = server_url.join(&full_rock_name)?;
460 let response = reqwest::Client::new().get(url.clone()).send().await?;
461 let bytes = if response.status().is_success() {
462 response.bytes().await
463 } else {
464 match args.fallback_ext {
465 Some(ext) => {
466 let full_rock_name =
467 mk_packed_rock_name(package.name(), package.version(), ext);
468 let url = server_url.join(&full_rock_name)?;
469 reqwest::Client::new()
470 .get(url.clone())
471 .send()
472 .await?
473 .error_for_status()?
474 .bytes()
475 .await
476 }
477 None => response.error_for_status()?.bytes().await,
478 }
479 }?;
480 Ok(DownloadedPackedRockBytes {
481 name: package.name().clone(),
482 version: package.version().clone(),
483 bytes,
484 file_name: full_rock_name,
485 url,
486 })
487 }
488}
489
490fn mk_packed_rock_name(name: &PackageName, version: &PackageVersion, ext: &str) -> String {
491 format!("{name}-{version}.{ext}")
492}
493
494pub(crate) async fn unpack_rockspec(
495 rock: &DownloadedPackedRockBytes,
496) -> Result<RemoteLuaRockspec, SearchAndDownloadError> {
497 let cursor = Cursor::new(&rock.bytes);
498 let rockspec_file_name = format!("{}-{}.rockspec", rock.name, rock.version);
499 let mut zip = zip::ZipArchive::new(cursor)
500 .map_err(|err| SearchAndDownloadError::ZipRead(rock.file_name.clone(), err))?;
501 let rockspec_index = (0..zip.len())
502 .find(|&i| {
503 unsafe { zip.by_index(i).unwrap_unchecked() }
504 .name()
505 .eq(&rockspec_file_name)
506 })
507 .ok_or(SearchAndDownloadError::RockspecNotFoundInPackedRock(
508 rockspec_file_name,
509 ))?;
510 let mut rockspec_file = zip
511 .by_index(rockspec_index)
512 .map_err(|err| SearchAndDownloadError::ZipExtract(rock.file_name.clone(), err))?;
513 let mut content = String::new();
514 rockspec_file.read_to_string(&mut content)?;
515 let rockspec = RemoteLuaRockspec::new(&content)?;
516 Ok(rockspec)
517}