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