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