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