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