]> git.proxmox.com Git - proxmox-offline-mirror.git/blame - src/mirror.rs
clippy fixup
[proxmox-offline-mirror.git] / src / mirror.rs
CommitLineData
9ecde319
FG
1use std::{
2 cmp::max,
3 collections::HashMap,
4 io::Read,
5 path::{Path, PathBuf},
6};
7
8b267808 8use anyhow::{bail, format_err, Error};
9ecde319 9use flate2::bufread::GzDecoder;
837867ed 10use globset::{Glob, GlobSet, GlobSetBuilder};
d035ecb5 11use nix::libc;
49997188 12use proxmox_http::{client::sync::Client, HttpClient, HttpOptions};
d035ecb5
FG
13use proxmox_sys::fs::file_get_contents;
14
15use crate::{
e79308e6 16 config::{MirrorConfig, SkipConfig, SubscriptionKey},
d035ecb5
FG
17 convert_repo_line,
18 pool::Pool,
529111dc 19 types::{Diff, Snapshot, SNAPSHOT_REGEX},
d035ecb5
FG
20 FetchResult, Progress,
21};
9ecde319
FG
22use proxmox_apt::{
23 deb822::{
24 CheckSums, CompressionType, FileReference, FileReferenceType, PackagesFile, ReleaseFile,
67c0b91c 25 SourcesFile,
9ecde319
FG
26 },
27 repositories::{APTRepository, APTRepositoryPackageType},
28};
29
30use crate::helpers;
31
c598cb15
FG
32fn mirror_dir(config: &MirrorConfig) -> String {
33 format!("{}/{}", config.base_dir, config.id)
34}
35
d035ecb5 36pub(crate) fn pool(config: &MirrorConfig) -> Result<Pool, Error> {
c598cb15
FG
37 let pool_dir = format!("{}/.pool", config.base_dir);
38 Pool::open(Path::new(&mirror_dir(config)), Path::new(&pool_dir))
d035ecb5
FG
39}
40
2d13dcfc 41/// `MirrorConfig`, but some fields converted/parsed into usable types.
d035ecb5
FG
42struct ParsedMirrorConfig {
43 pub repository: APTRepository,
44 pub architectures: Vec<String>,
45 pub pool: Pool,
46 pub key: Vec<u8>,
47 pub verify: bool,
48 pub sync: bool,
8b267808 49 pub auth: Option<String>,
49997188 50 pub client: Client,
96a80415 51 pub ignore_errors: bool,
e79308e6 52 pub skip: SkipConfig,
d035ecb5
FG
53}
54
55impl TryInto<ParsedMirrorConfig> for MirrorConfig {
56 type Error = anyhow::Error;
57
58 fn try_into(self) -> Result<ParsedMirrorConfig, Self::Error> {
59 let pool = pool(&self)?;
60
61 let repository = convert_repo_line(self.repository.clone())?;
62
63 let key = file_get_contents(Path::new(&self.key_path))?;
64
49997188
FG
65 let options = HttpOptions {
66 user_agent: Some("proxmox-offline-mirror 0.1".to_string()),
67 ..Default::default()
68 }; // TODO actually read version ;)
69
70 let client = Client::new(options);
8b267808 71
d035ecb5
FG
72 Ok(ParsedMirrorConfig {
73 repository,
74 architectures: self.architectures,
75 pool,
76 key,
77 verify: self.verify,
78 sync: self.sync,
8b267808 79 auth: None,
49997188 80 client,
96a80415 81 ignore_errors: self.ignore_errors,
e79308e6 82 skip: self.skip,
d035ecb5
FG
83 })
84 }
85}
86
2d13dcfc 87// Helper to get absolute URL for dist-specific relative `path`.
9ecde319
FG
88fn get_dist_url(repo: &APTRepository, path: &str) -> String {
89 let dist_root = format!("{}/dists/{}", repo.uris[0], repo.suites[0]);
90
91 format!("{}/{}", dist_root, path)
92}
93
2d13dcfc 94// Helper to get dist-specific path given a `prefix` (snapshot dir) and relative `path`.
9ecde319
FG
95fn get_dist_path(repo: &APTRepository, prefix: &Path, path: &str) -> PathBuf {
96 let mut base = PathBuf::from(prefix);
97 base.push("dists");
98 base.push(&repo.suites[0]);
99 base.push(path);
100 base
101}
102
2d13dcfc 103// Helper to get generic URL given a `repo` and `path`.
9ecde319
FG
104fn get_repo_url(repo: &APTRepository, path: &str) -> String {
105 format!("{}/{}", repo.uris[0], path)
106}
107
2d13dcfc
FG
108/// Helper to fetch file from URI and optionally verify the responses checksum.
109///
110/// Only fetches and returns data, doesn't store anything anywhere.
9ecde319 111fn fetch_repo_file(
49997188 112 client: &Client,
9ecde319 113 uri: &str,
d7e210ac 114 max_size: usize,
9ecde319 115 checksums: Option<&CheckSums>,
8b267808 116 auth: Option<&str>,
9ecde319
FG
117) -> Result<FetchResult, Error> {
118 println!("-> GET '{}'..", uri);
119
49997188
FG
120 let headers = if let Some(auth) = auth {
121 let mut map = HashMap::new();
122 map.insert("Authorization".to_string(), auth.to_string());
123 Some(map)
8b267808 124 } else {
49997188 125 None
8b267808
FG
126 };
127
49997188 128 let response = client.get(uri, headers.as_ref())?;
9ecde319 129
49997188 130 let reader: Box<dyn Read> = response.into_body();
d7e210ac 131 let mut reader = reader.take(max_size as u64);
9ecde319 132 let mut data = Vec::new();
49997188 133 reader.read_to_end(&mut data)?;
9ecde319
FG
134
135 if let Some(checksums) = checksums {
136 checksums.verify(&data)?;
137 }
138
139 Ok(FetchResult {
49997188 140 fetched: data.len(),
9ecde319 141 data,
9ecde319
FG
142 })
143}
144
36949d11 145/// Helper to fetch InRelease or Release/Release.gpg files from repository.
2d13dcfc 146///
36949d11 147/// Set `detached` == false to fetch InRelease or to `detached` == true for Release/Release.gpg.
7c17509f 148/// Verifies the contained/detached signature and stores all fetched files under `prefix`.
837867ed 149///
7c17509f 150/// Returns the verified raw release file data, or None if the "fetch" part itself fails.
9ecde319
FG
151fn fetch_release(
152 config: &ParsedMirrorConfig,
153 prefix: &Path,
154 detached: bool,
d2757931 155 dry_run: bool,
7c17509f 156) -> Result<Option<FetchResult>, Error> {
9ecde319
FG
157 let (name, fetched, sig) = if detached {
158 println!("Fetching Release/Release.gpg files");
7c17509f 159 let sig = match fetch_repo_file(
49997188 160 &config.client,
8b267808 161 &get_dist_url(&config.repository, "Release.gpg"),
d7e210ac 162 1024 * 1024,
8b267808
FG
163 None,
164 config.auth.as_deref(),
7c17509f
FG
165 ) {
166 Ok(res) => res,
167 Err(err) => {
168 eprintln!("Release.gpg fetch failure: {err}");
169 return Ok(None);
170 }
171 };
172
173 let mut fetched = match fetch_repo_file(
49997188 174 &config.client,
9ecde319 175 &get_dist_url(&config.repository, "Release"),
d7e210ac 176 256 * 1024 * 1024,
9ecde319 177 None,
8b267808 178 config.auth.as_deref(),
7c17509f
FG
179 ) {
180 Ok(res) => res,
181 Err(err) => {
182 eprintln!("Release fetch failure: {err}");
183 return Ok(None);
184 }
185 };
9ecde319
FG
186 fetched.fetched += sig.fetched;
187 ("Release(.gpg)", fetched, Some(sig.data()))
188 } else {
189 println!("Fetching InRelease file");
7c17509f 190 let fetched = match fetch_repo_file(
49997188 191 &config.client,
9ecde319 192 &get_dist_url(&config.repository, "InRelease"),
d7e210ac 193 256 * 1024 * 1024,
9ecde319 194 None,
8b267808 195 config.auth.as_deref(),
7c17509f
FG
196 ) {
197 Ok(res) => res,
198 Err(err) => {
199 eprintln!("InRelease fetch failure: {err}");
200 return Ok(None);
201 }
202 };
9ecde319
FG
203 ("InRelease", fetched, None)
204 };
205
206 println!("Verifying '{name}' signature using provided repository key..");
207 let content = fetched.data_ref();
208 let verified = helpers::verify_signature(content, &config.key, sig.as_deref())?;
209 println!("Success");
210
211 let sha512 = Some(openssl::sha::sha512(content));
212 let csums = CheckSums {
213 sha512,
214 ..Default::default()
215 };
216
d2757931 217 if dry_run {
7c17509f 218 return Ok(Some(FetchResult {
d2757931
FG
219 data: verified,
220 fetched: fetched.fetched,
7c17509f 221 }));
d2757931
FG
222 }
223
9ecde319
FG
224 let locked = &config.pool.lock()?;
225
226 if !locked.contains(&csums) {
d035ecb5 227 locked.add_file(content, &csums, config.sync)?;
9ecde319
FG
228 }
229
230 if detached {
231 locked.link_file(
232 &csums,
233 Path::new(&get_dist_path(&config.repository, prefix, "Release")),
234 )?;
235 let sig = sig.unwrap();
236 let sha512 = Some(openssl::sha::sha512(&sig));
237 let csums = CheckSums {
238 sha512,
239 ..Default::default()
240 };
241 if !locked.contains(&csums) {
d035ecb5 242 locked.add_file(&sig, &csums, config.sync)?;
9ecde319
FG
243 }
244 locked.link_file(
245 &csums,
246 Path::new(&get_dist_path(&config.repository, prefix, "Release.gpg")),
247 )?;
248 } else {
249 locked.link_file(
250 &csums,
251 Path::new(&get_dist_path(&config.repository, prefix, "InRelease")),
252 )?;
253 }
254
7c17509f 255 Ok(Some(FetchResult {
9ecde319
FG
256 data: verified,
257 fetched: fetched.fetched,
7c17509f 258 }))
9ecde319
FG
259}
260
2d13dcfc
FG
261/// Helper to fetch an index file referenced by a `ReleaseFile`.
262///
36949d11
TL
263/// Since these usually come in compressed and uncompressed form, with the latter often not
264/// actually existing in the source repository as file, this fetches and if necessary decompresses
265/// to obtain a copy of the uncompressed data.
266/// Will skip fetching if both references are already available with the expected checksum in the
267/// pool, in which case they will just be re-linked under the new path.
2d13dcfc
FG
268///
269/// Returns the uncompressed data.
9ecde319
FG
270fn fetch_index_file(
271 config: &ParsedMirrorConfig,
272 prefix: &Path,
273 reference: &FileReference,
c5fed38d 274 uncompressed: Option<&FileReference>,
8063fd36 275 by_hash: bool,
d2757931 276 dry_run: bool,
9ecde319
FG
277) -> Result<FetchResult, Error> {
278 let url = get_dist_url(&config.repository, &reference.path);
279 let path = get_dist_path(&config.repository, prefix, &reference.path);
c5fed38d
FG
280
281 if let Some(uncompressed) = uncompressed {
282 let uncompressed_path = get_dist_path(&config.repository, prefix, &uncompressed.path);
283
284 if config.pool.contains(&reference.checksums)
285 && config.pool.contains(&uncompressed.checksums)
286 {
287 let data = config
288 .pool
289 .get_contents(&uncompressed.checksums, config.verify)?;
290
d2757931
FG
291 if dry_run {
292 return Ok(FetchResult { data, fetched: 0 });
293 }
c5fed38d
FG
294 // Ensure they're linked at current path
295 config.pool.lock()?.link_file(&reference.checksums, &path)?;
296 config
297 .pool
298 .lock()?
299 .link_file(&uncompressed.checksums, &uncompressed_path)?;
300 return Ok(FetchResult { data, fetched: 0 });
301 }
9ecde319
FG
302 }
303
8063fd36
FG
304 let urls = if by_hash {
305 let mut urls = Vec::new();
306 if let Some((base_url, _file_name)) = url.rsplit_once('/') {
307 if let Some(sha512) = reference.checksums.sha512 {
308 urls.push(format!("{base_url}/by-hash/SHA512/{}", hex::encode(sha512)));
309 }
310 if let Some(sha256) = reference.checksums.sha256 {
311 urls.push(format!("{base_url}/by-hash/SHA256/{}", hex::encode(sha256)));
312 }
313 }
314 urls.push(url);
315 urls
316 } else {
317 vec![url]
318 };
319
320 let res = urls
321 .iter()
322 .fold(None, |res, url| match res {
323 Some(Ok(res)) => Some(Ok(res)),
324 _ => Some(fetch_plain_file(
325 config,
326 url,
327 &path,
328 reference.size,
329 &reference.checksums,
330 true,
d2757931 331 dry_run,
8063fd36
FG
332 )),
333 })
334 .ok_or_else(|| format_err!("Failed to retrieve {}", reference.path))??;
9ecde319
FG
335
336 let mut buf = Vec::new();
337 let raw = res.data_ref();
338
339 let decompressed = match reference.file_type.compression() {
340 None => raw,
341 Some(CompressionType::Gzip) => {
342 let mut gz = GzDecoder::new(raw);
343 gz.read_to_end(&mut buf)?;
344 &buf[..]
345 }
346 Some(CompressionType::Bzip2) => {
347 let mut bz = bzip2::read::BzDecoder::new(raw);
348 bz.read_to_end(&mut buf)?;
349 &buf[..]
350 }
351 Some(CompressionType::Lzma) | Some(CompressionType::Xz) => {
bb1685a0 352 let mut xz = xz2::read::XzDecoder::new_multi_decoder(raw);
9ecde319
FG
353 xz.read_to_end(&mut buf)?;
354 &buf[..]
355 }
356 };
d2757931
FG
357 let res = FetchResult {
358 data: decompressed.to_owned(),
359 fetched: res.fetched,
360 };
361
362 if dry_run {
363 return Ok(res);
364 }
9ecde319
FG
365
366 let locked = &config.pool.lock()?;
c5fed38d
FG
367 if let Some(uncompressed) = uncompressed {
368 if !locked.contains(&uncompressed.checksums) {
369 locked.add_file(decompressed, &uncompressed.checksums, config.sync)?;
370 }
9ecde319 371
c5fed38d
FG
372 // Ensure it's linked at current path
373 let uncompressed_path = get_dist_path(&config.repository, prefix, &uncompressed.path);
374 locked.link_file(&uncompressed.checksums, &uncompressed_path)?;
375 }
9ecde319 376
d2757931 377 Ok(res)
9ecde319
FG
378}
379
2d13dcfc
FG
380/// Helper to fetch arbitrary files like binary packages.
381///
36949d11
TL
382/// Will skip fetching if matching file already exists locally, in which case it will just be
383/// re-linked under the new path.
2d13dcfc 384///
36949d11
TL
385/// If need_data is false and the mirror config is set to skip verification, reading the file's
386/// content will be skipped as well if fetching was skipped.
9ecde319
FG
387fn fetch_plain_file(
388 config: &ParsedMirrorConfig,
389 url: &str,
390 file: &Path,
d7e210ac 391 max_size: usize,
9ecde319
FG
392 checksums: &CheckSums,
393 need_data: bool,
d2757931 394 dry_run: bool,
9ecde319
FG
395) -> Result<FetchResult, Error> {
396 let locked = &config.pool.lock()?;
397 let res = if locked.contains(checksums) {
398 if need_data || config.verify {
399 locked
400 .get_contents(checksums, config.verify)
401 .map(|data| FetchResult { data, fetched: 0 })?
402 } else {
403 // performance optimization for .deb files if verify is false
404 // we never need the file contents and they make up the bulk of a repo
405 FetchResult {
406 data: vec![],
407 fetched: 0,
408 }
409 }
d2757931
FG
410 } else if dry_run && !need_data {
411 FetchResult {
412 data: vec![],
413 fetched: 0,
414 }
9ecde319 415 } else {
8b267808 416 let fetched = fetch_repo_file(
49997188 417 &config.client,
8b267808 418 url,
d7e210ac 419 max_size,
8b267808
FG
420 Some(checksums),
421 config.auth.as_deref(),
422 )?;
9ecde319
FG
423 locked.add_file(fetched.data_ref(), checksums, config.verify)?;
424 fetched
425 };
426
d2757931
FG
427 if !dry_run {
428 // Ensure it's linked at current path
429 locked.link_file(checksums, file)?;
430 }
9ecde319
FG
431
432 Ok(res)
433}
434
2d13dcfc 435/// Initialize a new mirror (by creating the corresponding pool).
d035ecb5 436pub fn init(config: &MirrorConfig) -> Result<(), Error> {
c598cb15
FG
437 let pool_dir = format!("{}/.pool", config.base_dir);
438
439 let dir = format!("{}/{}", config.base_dir, config.id);
440
441 Pool::create(Path::new(&dir), Path::new(&pool_dir))?;
d035ecb5
FG
442 Ok(())
443}
444
c598cb15 445/// Destroy a mirror (by destroying the corresponding pool's link dir followed by GC).
d035ecb5
FG
446pub fn destroy(config: &MirrorConfig) -> Result<(), Error> {
447 let pool: Pool = pool(config)?;
448 pool.lock()?.destroy()?;
449
450 Ok(())
451}
452
2d13dcfc 453/// List snapshots
d035ecb5
FG
454pub fn list_snapshots(config: &MirrorConfig) -> Result<Vec<Snapshot>, Error> {
455 let _pool: Pool = pool(config)?;
456
457 let mut list: Vec<Snapshot> = vec![];
458
c598cb15
FG
459 let dir = mirror_dir(config);
460
461 let path = Path::new(&dir);
d035ecb5
FG
462
463 proxmox_sys::fs::scandir(
464 libc::AT_FDCWD,
465 path,
466 &SNAPSHOT_REGEX,
467 |_l2_fd, snapshot, file_type| {
468 if file_type != nix::dir::Type::Directory {
469 return Ok(());
470 }
471
472 list.push(snapshot.parse()?);
473
474 Ok(())
475 },
476 )?;
477
45aa8bea
FG
478 list.sort_unstable();
479
d035ecb5
FG
480 Ok(list)
481}
482
837867ed
FG
483struct MirrorProgress {
484 warnings: Vec<String>,
485 dry_run: Progress,
486 total: Progress,
487 skip_count: usize,
488 skip_bytes: usize,
489}
490
491fn convert_to_globset(config: &ParsedMirrorConfig) -> Result<Option<GlobSet>, Error> {
492 Ok(if let Some(skipped_packages) = &config.skip.skip_packages {
493 let mut globs = GlobSetBuilder::new();
494 for glob in skipped_packages {
495 let glob = Glob::new(glob)?;
496 globs.add(glob);
497 }
498 let globs = globs.build()?;
499 Some(globs)
500 } else {
501 None
502 })
503}
504
505fn fetch_binary_packages(
506 config: &ParsedMirrorConfig,
69585027 507 component: &str,
837867ed
FG
508 packages_indices: HashMap<&String, PackagesFile>,
509 dry_run: bool,
510 prefix: &Path,
511 progress: &mut MirrorProgress,
512) -> Result<(), Error> {
513 let skipped_package_globs = convert_to_globset(config)?;
514
515 for (basename, references) in packages_indices {
516 let total_files = references.files.len();
517 if total_files == 0 {
518 println!("\n{basename} - no files, skipping.");
519 continue;
520 } else {
521 println!("\n{basename} - {total_files} total file(s)");
522 }
523
524 let mut fetch_progress = Progress::new();
525 let mut skip_count = 0usize;
526 let mut skip_bytes = 0usize;
69585027 527
837867ed
FG
528 for package in references.files {
529 if let Some(ref sections) = &config.skip.skip_sections {
69585027
FG
530 if sections.iter().any(|section| {
531 package.section == *section
532 || package.section == format!("{component}/{section}")
533 }) {
837867ed
FG
534 println!(
535 "\tskipping {} - {}b (section '{}')",
536 package.package, package.size, package.section
537 );
538 skip_count += 1;
539 skip_bytes += package.size;
540 continue;
541 }
542 }
543 if let Some(skipped_package_globs) = &skipped_package_globs {
544 let matches = skipped_package_globs.matches(&package.package);
545 if !matches.is_empty() {
546 // safety, skipped_package_globs is set based on this
547 let globs = config.skip.skip_packages.as_ref().unwrap();
548 let matches: Vec<String> = matches.iter().map(|i| globs[*i].clone()).collect();
549 println!(
550 "\tskipping {} - {}b (package glob(s): {})",
551 package.package,
552 package.size,
553 matches.join(", ")
554 );
555 skip_count += 1;
556 skip_bytes += package.size;
557 continue;
558 }
559 }
560 let url = get_repo_url(&config.repository, &package.file);
561
562 if dry_run {
563 if config.pool.contains(&package.checksums) {
564 fetch_progress.update(&FetchResult {
565 data: vec![],
566 fetched: 0,
567 });
568 } else {
569 println!("\t(dry-run) GET missing '{url}' ({}b)", package.size);
570 fetch_progress.update(&FetchResult {
571 data: vec![],
572 fetched: package.size,
573 });
574 }
575 } else {
576 let mut full_path = PathBuf::from(prefix);
577 full_path.push(&package.file);
578
579 match fetch_plain_file(
580 config,
581 &url,
582 &full_path,
583 package.size,
584 &package.checksums,
585 false,
586 dry_run,
587 ) {
588 Ok(res) => fetch_progress.update(&res),
589 Err(err) if config.ignore_errors => {
590 let msg = format!(
591 "{}: failed to fetch package '{}' - {}",
592 basename, package.file, err,
593 );
594 eprintln!("{msg}");
595 progress.warnings.push(msg);
596 }
597 Err(err) => return Err(err),
598 }
599 }
600
601 if fetch_progress.file_count() % (max(total_files / 100, 1)) == 0 {
602 println!("\tProgress: {fetch_progress}");
603 }
604 }
605 println!("\tProgress: {fetch_progress}");
606 if dry_run {
607 progress.dry_run += fetch_progress;
608 } else {
609 progress.total += fetch_progress;
610 }
611 if skip_count > 0 {
612 progress.skip_count += skip_count;
613 progress.skip_bytes += skip_bytes;
614 println!("Skipped downloading {skip_count} packages totalling {skip_bytes}b");
615 }
616 }
617
618 Ok(())
619}
620
621fn fetch_source_packages(
622 config: &ParsedMirrorConfig,
69585027 623 component: &str,
837867ed
FG
624 source_packages_indices: HashMap<&String, SourcesFile>,
625 dry_run: bool,
626 prefix: &Path,
627 progress: &mut MirrorProgress,
628) -> Result<(), Error> {
629 let skipped_package_globs = convert_to_globset(config)?;
630
631 for (basename, references) in source_packages_indices {
632 let total_source_packages = references.source_packages.len();
633 if total_source_packages == 0 {
634 println!("\n{basename} - no files, skipping.");
635 continue;
636 } else {
637 println!("\n{basename} - {total_source_packages} total source package(s)");
638 }
639
640 let mut fetch_progress = Progress::new();
641 let mut skip_count = 0usize;
642 let mut skip_bytes = 0usize;
643 for package in references.source_packages {
644 if let Some(ref sections) = &config.skip.skip_sections {
69585027
FG
645 if sections.iter().any(|section| {
646 package.section.as_ref() == Some(section)
647 || package.section == Some(format!("{component}/{section}"))
648 }) {
837867ed
FG
649 println!(
650 "\tskipping {} - {}b (section '{}')",
651 package.package,
652 package.size(),
653 package.section.as_ref().unwrap(),
654 );
655 skip_count += 1;
656 skip_bytes += package.size();
657 continue;
658 }
659 }
660 if let Some(skipped_package_globs) = &skipped_package_globs {
661 let matches = skipped_package_globs.matches(&package.package);
662 if !matches.is_empty() {
663 // safety, skipped_package_globs is set based on this
664 let globs = config.skip.skip_packages.as_ref().unwrap();
665 let matches: Vec<String> = matches.iter().map(|i| globs[*i].clone()).collect();
666 println!(
667 "\tskipping {} - {}b (package glob(s): {})",
668 package.package,
669 package.size(),
670 matches.join(", ")
671 );
672 skip_count += 1;
673 skip_bytes += package.size();
674 continue;
675 }
676 }
677
678 for file_reference in package.files.values() {
679 let path = format!("{}/{}", package.directory, file_reference.file);
680 let url = get_repo_url(&config.repository, &path);
681
682 if dry_run {
683 if config.pool.contains(&file_reference.checksums) {
684 fetch_progress.update(&FetchResult {
685 data: vec![],
686 fetched: 0,
687 });
688 } else {
689 println!("\t(dry-run) GET missing '{url}' ({}b)", file_reference.size);
690 fetch_progress.update(&FetchResult {
691 data: vec![],
692 fetched: file_reference.size,
693 });
694 }
695 } else {
696 let mut full_path = PathBuf::from(prefix);
697 full_path.push(&path);
698
699 match fetch_plain_file(
700 config,
701 &url,
702 &full_path,
703 file_reference.size,
704 &file_reference.checksums,
705 false,
706 dry_run,
707 ) {
708 Ok(res) => fetch_progress.update(&res),
709 Err(err) if config.ignore_errors => {
710 let msg = format!(
711 "{}: failed to fetch package '{}' - {}",
712 basename, file_reference.file, err,
713 );
714 eprintln!("{msg}");
715 progress.warnings.push(msg);
716 }
717 Err(err) => return Err(err),
718 }
719 }
720
721 if fetch_progress.file_count() % (max(total_source_packages / 100, 1)) == 0 {
722 println!("\tProgress: {fetch_progress}");
723 }
724 }
725 }
726 println!("\tProgress: {fetch_progress}");
727 if dry_run {
728 progress.dry_run += fetch_progress;
729 } else {
730 progress.total += fetch_progress;
731 }
732 if skip_count > 0 {
733 progress.skip_count += skip_count;
734 progress.skip_bytes += skip_bytes;
735 println!("Skipped downloading {skip_count} packages totalling {skip_bytes}b");
736 }
737 }
738
739 Ok(())
740}
741
2d13dcfc
FG
742/// Create a new snapshot of the remote repository, fetching and storing files as needed.
743///
744/// Operates in three phases:
745/// - Fetch and verify release files
746/// - Fetch referenced indices according to config
747/// - Fetch binary packages referenced by package indices
748///
36949d11
TL
749/// Files will be linked in a temporary directory and only renamed to the final, valid snapshot
750/// directory at the end. In case of error, leftover `XXX.tmp` directories at the top level of
751/// `base_dir` can be safely removed once the next snapshot was successfully created, as they only
752/// contain hardlinks.
8b267808
FG
753pub fn create_snapshot(
754 config: MirrorConfig,
755 snapshot: &Snapshot,
756 subscription: Option<SubscriptionKey>,
d2757931 757 dry_run: bool,
8b267808
FG
758) -> Result<(), Error> {
759 let auth = if let Some(product) = &config.use_subscription {
760 match subscription {
761 None => {
762 bail!(
763 "Mirror {} requires a subscription key, but none given.",
764 config.id
765 );
766 }
767 Some(key) if key.product() == *product => {
768 let base64 = base64::encode(format!("{}:{}", key.key, key.server_id));
769 Some(format!("basic {base64}"))
770 }
771 Some(key) => {
772 bail!(
773 "Repository product type '{}' and key product type '{}' don't match.",
774 product,
775 key.product()
776 );
777 }
778 }
779 } else {
780 None
781 };
782
783 let mut config: ParsedMirrorConfig = config.try_into()?;
784 config.auth = auth;
9ecde319
FG
785
786 let prefix = format!("{snapshot}.tmp");
787 let prefix = Path::new(&prefix);
788
837867ed
FG
789 let mut progress = MirrorProgress {
790 warnings: Vec::new(),
791 skip_count: 0,
792 skip_bytes: 0,
793 dry_run: Progress::new(),
794 total: Progress::new(),
795 };
9ecde319
FG
796
797 let parse_release = |res: FetchResult, name: &str| -> Result<ReleaseFile, Error> {
798 println!("Parsing {name}..");
799 let parsed: ReleaseFile = res.data[..].try_into()?;
800 println!(
801 "'{name}' file has {} referenced files..",
802 parsed.files.len()
803 );
804 Ok(parsed)
805 };
806
7c17509f
FG
807 // we want both on-disk for compat reasons, if both are available
808 let release = fetch_release(&config, prefix, true, dry_run)?
809 .map(|res| {
837867ed 810 progress.total.update(&res);
7c17509f
FG
811 parse_release(res, "Release")
812 })
813 .transpose()?;
814
815 let in_release = fetch_release(&config, prefix, false, dry_run)?
816 .map(|res| {
837867ed 817 progress.total.update(&res);
7c17509f
FG
818 parse_release(res, "InRelease")
819 })
820 .transpose()?;
9ecde319 821
7c17509f
FG
822 // at least one must be available to proceed
823 let release = release
824 .or(in_release)
825 .ok_or_else(|| format_err!("Neither Release(.gpg) nor InRelease available!"))?;
9ecde319
FG
826
827 let mut per_component = HashMap::new();
828 let mut others = Vec::new();
829 let binary = &config
830 .repository
831 .types
832 .contains(&APTRepositoryPackageType::Deb);
833 let source = &config
834 .repository
835 .types
836 .contains(&APTRepositoryPackageType::DebSrc);
837
838 for (basename, references) in &release.files {
839 let reference = references.first();
840 let reference = if let Some(reference) = reference {
841 reference.clone()
842 } else {
843 continue;
844 };
845 let skip_components = !&config.repository.components.contains(&reference.component);
846
847 let skip = skip_components
848 || match &reference.file_type {
849 FileReferenceType::Ignored => true,
850 FileReferenceType::PDiff => true, // would require fetching the patches as well
9ecde319 851 FileReferenceType::Sources(_) => !source,
8a876c01
FG
852 _ => {
853 if let Some(arch) = reference.file_type.architecture() {
854 !binary || !config.architectures.contains(arch)
855 } else {
856 false
857 }
858 }
9ecde319
FG
859 };
860 if skip {
861 println!("Skipping {}", reference.path);
862 others.push(reference);
863 } else {
864 let list = per_component
865 .entry(reference.component)
866 .or_insert_with(Vec::new);
867 list.push(basename);
868 }
869 }
870 println!();
871
872 let mut indices_size = 0_usize;
873 let mut total_count = 0;
874
875 for (component, references) in &per_component {
876 println!("Component '{component}'");
877
878 let mut component_indices_size = 0;
879
880 for basename in references {
881 for reference in release.files.get(*basename).unwrap() {
882 println!("\t{:?}: {:?}", reference.path, reference.file_type);
883 component_indices_size += reference.size;
884 }
885 }
886 indices_size += component_indices_size;
887
888 let component_count = references.len();
889 total_count += component_count;
890
891 println!("Component references count: {component_count}");
892 println!("Component indices size: {component_indices_size}");
893 if references.is_empty() {
894 println!("\tNo references found..");
895 }
896 }
897 println!("Total indices count: {total_count}");
898 println!("Total indices size: {indices_size}");
899
900 if !others.is_empty() {
901 println!("Skipped {} references", others.len());
902 }
903 println!();
904
905 let mut packages_size = 0_usize;
f4d89ed7 906 #[allow(clippy::type_complexity)]
69585027
FG
907 let mut per_component_indices: HashMap<
908 String,
909 (
910 HashMap<&String, PackagesFile>,
911 HashMap<&String, SourcesFile>,
912 ),
913 > = HashMap::new();
67c0b91c 914
7829ab74 915 let mut failed_references = Vec::new();
9ecde319
FG
916 for (component, references) in per_component {
917 println!("\nFetching indices for component '{component}'");
918 let mut component_deb_size = 0;
67c0b91c
FG
919 let mut component_dsc_size = 0;
920
9ecde319
FG
921 let mut fetch_progress = Progress::new();
922
69585027
FG
923 let (packages_indices, source_packages_indices) =
924 per_component_indices.entry(component.clone()).or_default();
925
9ecde319
FG
926 for basename in references {
927 println!("\tFetching '{basename}'..");
928 let files = release.files.get(basename).unwrap();
c5fed38d
FG
929 let uncompressed_ref = files.iter().find(|reference| reference.path == *basename);
930
9ecde319
FG
931 let mut package_index_data = None;
932
933 for reference in files {
36949d11
TL
934 // if both compressed and uncompressed are referenced, the uncompressed file may
935 // not exist on the server
c5fed38d 936 if Some(reference) == uncompressed_ref && files.len() > 1 {
9ecde319
FG
937 continue;
938 }
939
940 // this will ensure the uncompressed file will be written locally
8063fd36
FG
941 let res = match fetch_index_file(
942 &config,
943 prefix,
944 reference,
945 uncompressed_ref,
946 release.aquire_by_hash,
d2757931 947 dry_run,
8063fd36 948 ) {
7829ab74
FG
949 Ok(res) => res,
950 Err(err) if !reference.file_type.is_package_index() => {
9213b79a 951 let msg = format!(
7829ab74
FG
952 "Failed to fetch '{:?}' type reference '{}', skipping - {err}",
953 reference.file_type, reference.path
954 );
9213b79a 955 eprintln!("{msg}");
837867ed 956 progress.warnings.push(msg);
7829ab74
FG
957 failed_references.push(reference);
958 continue;
959 }
36dfc650 960 Err(err) => return Err(err),
7829ab74 961 };
9ecde319
FG
962 fetch_progress.update(&res);
963
964 if package_index_data.is_none() && reference.file_type.is_package_index() {
67c0b91c 965 package_index_data = Some((&reference.file_type, res.data()));
9ecde319
FG
966 }
967 }
67c0b91c
FG
968 if let Some((reference_type, data)) = package_index_data {
969 match reference_type {
970 FileReferenceType::Packages(_, _) => {
971 let packages: PackagesFile = data[..].try_into()?;
972 let size: usize = packages.files.iter().map(|p| p.size).sum();
973 println!("\t{} packages totalling {size}", packages.files.len());
974 component_deb_size += size;
975
976 packages_indices.entry(basename).or_insert(packages);
977 }
978 FileReferenceType::Sources(_) => {
979 let source_packages: SourcesFile = data[..].try_into()?;
980 let size: usize = source_packages
981 .source_packages
982 .iter()
983 .map(|s| s.size())
984 .sum();
985 println!(
986 "\t{} source packages totalling {size}",
987 source_packages.source_packages.len()
988 );
989 component_dsc_size += size;
990 source_packages_indices
991 .entry(basename)
992 .or_insert(source_packages);
993 }
994 unknown => {
995 eprintln!("Unknown package index '{unknown:?}', skipping processing..")
996 }
997 }
9ecde319
FG
998 }
999 println!("Progress: {fetch_progress}");
1000 }
67c0b91c 1001
9ecde319
FG
1002 println!("Total deb size for component: {component_deb_size}");
1003 packages_size += component_deb_size;
67c0b91c
FG
1004
1005 println!("Total dsc size for component: {component_dsc_size}");
1006 packages_size += component_dsc_size;
1007
837867ed 1008 progress.total += fetch_progress;
9ecde319
FG
1009 }
1010 println!("Total deb size: {packages_size}");
7829ab74
FG
1011 if !failed_references.is_empty() {
1012 eprintln!("Failed to download non-package-index references:");
1013 for reference in failed_references {
1014 eprintln!("\t{}", reference.path);
1015 }
1016 }
9ecde319 1017
69585027
FG
1018 for (component, (packages_indices, source_packages_indices)) in per_component_indices {
1019 println!("\nFetching {component} packages..");
1020 fetch_binary_packages(
1021 &config,
1022 &component,
1023 packages_indices,
1024 dry_run,
1025 prefix,
1026 &mut progress,
1027 )?;
67c0b91c 1028
69585027
FG
1029 fetch_source_packages(
1030 &config,
1031 &component,
1032 source_packages_indices,
1033 dry_run,
1034 prefix,
1035 &mut progress,
1036 )?;
1037 }
9ecde319 1038
d2757931 1039 if dry_run {
837867ed
FG
1040 println!(
1041 "\nDry-run Stats (indices, downloaded but not persisted):\n{}",
1042 progress.total
1043 );
1044 println!(
1045 "\nDry-run stats (packages, new == missing):\n{}",
1046 progress.dry_run
1047 );
d2757931 1048 } else {
837867ed 1049 println!("\nStats: {}", progress.total);
d2757931 1050 }
e79308e6
FG
1051 if total_count > 0 {
1052 println!(
837867ed
FG
1053 "Skipped downloading {} packages totalling {}b",
1054 progress.skip_count, progress.skip_bytes,
e79308e6
FG
1055 );
1056 }
9ecde319 1057
837867ed 1058 if !progress.warnings.is_empty() {
9213b79a 1059 eprintln!("Warnings:");
837867ed 1060 for msg in progress.warnings {
9213b79a
FG
1061 eprintln!("- {msg}");
1062 }
1063 }
1064
d2757931 1065 if !dry_run {
9213b79a 1066 println!("\nRotating temp. snapshot in-place: {prefix:?} -> \"{snapshot}\"");
d2757931
FG
1067 let locked = config.pool.lock()?;
1068 locked.rename(prefix, Path::new(&format!("{snapshot}")))?;
1069 }
9ecde319
FG
1070
1071 Ok(())
1072}
d035ecb5 1073
36949d11
TL
1074/// Remove a snapshot by removing the corresponding snapshot directory. To actually free up space,
1075/// a garbage collection needs to be run afterwards.
d035ecb5
FG
1076pub fn remove_snapshot(config: &MirrorConfig, snapshot: &Snapshot) -> Result<(), Error> {
1077 let pool: Pool = pool(config)?;
1078 let path = pool.get_path(Path::new(&snapshot.to_string()))?;
1079
1080 pool.lock()?.remove_dir(&path)
1081}
1082
2d13dcfc 1083/// Run a garbage collection on the underlying pool.
d035ecb5
FG
1084pub fn gc(config: &MirrorConfig) -> Result<(usize, u64), Error> {
1085 let pool: Pool = pool(config)?;
1086
1087 pool.lock()?.gc()
1088}
529111dc
FG
1089
1090/// Print differences between two snapshots
1091pub fn diff_snapshots(
1092 config: &MirrorConfig,
1093 snapshot: &Snapshot,
1094 other_snapshot: &Snapshot,
1095) -> Result<Diff, Error> {
1096 let pool = pool(config)?;
1097 pool.lock()?.diff_dirs(
1098 Path::new(&format!("{snapshot}")),
1099 Path::new(&format!("{other_snapshot}")),
1100 )
1101}