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