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