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