1mod layout;
12
13use crate::check::digest_file;
14use crate::fetch::{sanitize, Fetcher, Progress};
15use crate::mirror::MirrorRead;
16use crate::{
17 config, mirror, Artifact, ArtifactKey, CheckFailure, ContentCheck, CredentialProvider,
18 DatastoreError, Mirror, Result,
19};
20use layout::{now_unix, Layout};
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23use std::cell::Cell;
24use std::collections::{HashMap, HashSet};
25use std::io::Write;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29use tempfile::NamedTempFile;
30
31const PREFIX_BYTES: usize = 8 * 1024;
34
35const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Layer {
41 LocalDisk,
42 Mirror,
43 Upstream,
44}
45
46#[derive(Debug, Clone)]
47pub struct ResolveOutcome {
48 pub layer: Layer,
49 pub bytes: u64,
50 pub duration: Duration,
51 pub source_index: Option<usize>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct IndexEntry {
59 pub digest: String,
61 pub bytes: u64,
62 pub fetched_at: u64,
64 pub source: Option<String>,
67 pub etag: Option<String>,
68 pub provider_identity: Option<String>,
69 pub layer: Layer,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct VerifyFailure {
75 pub key: ArtifactKey,
76 pub expected: String,
78 pub actual: Option<String>,
80 pub reason: String,
83}
84
85impl std::fmt::Display for VerifyFailure {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 write!(f, "{}: {}", self.key, self.reason)
88 }
89}
90
91pub type ProgressFn = dyn Fn(u64, Option<u64>) + Send + Sync;
93
94pub struct Datastore {
95 layout: Layout,
96 mirror: Option<Box<dyn MirrorRead>>,
97 fetcher: Fetcher,
98 allow_upstream: bool,
99 offline: bool,
100 max_bytes: Option<u64>,
101}
102
103#[derive(Default)]
104pub struct DatastoreBuilder {
105 pub(crate) cache_root: Option<PathBuf>,
106 pub(crate) mirror: Option<Mirror>,
107 pub(crate) credentials: Option<Box<dyn CredentialProvider>>,
108 pub(crate) allow_upstream: Option<bool>,
109 pub(crate) offline: Option<bool>,
110 pub(crate) timeout: Option<Duration>,
111 pub(crate) progress: Option<bool>,
112 pub(crate) on_progress: Option<Arc<ProgressFn>>,
113 pub(crate) max_bytes: Option<u64>,
114}
115
116impl DatastoreBuilder {
117 pub fn from_env() -> Result<Self> {
120 config::apply(Self::default())
121 }
122
123 pub fn cache_root(mut self, path: PathBuf) -> Self {
124 self.cache_root = Some(path);
125 self
126 }
127
128 pub fn mirror(mut self, mirror: Mirror) -> Self {
129 self.mirror = Some(mirror);
130 self
131 }
132
133 pub fn without_mirror(mut self) -> Self {
136 self.mirror = None;
137 self
138 }
139
140 pub fn credentials(mut self, provider: Box<dyn CredentialProvider>) -> Self {
141 self.credentials = Some(provider);
142 self
143 }
144
145 pub fn allow_upstream(mut self, allow: bool) -> Self {
147 self.allow_upstream = Some(allow);
148 self
149 }
150
151 pub fn offline(mut self, offline: bool) -> Self {
154 self.offline = Some(offline);
155 self
156 }
157
158 pub fn timeout(mut self, timeout: Duration) -> Self {
161 self.timeout = Some(timeout);
162 self
163 }
164
165 pub fn progress(mut self, enabled: bool) -> Self {
168 self.progress = Some(enabled);
169 self
170 }
171
172 pub fn on_progress(mut self, callback: Box<ProgressFn>) -> Self {
174 self.on_progress = Some(Arc::from(callback));
175 self
176 }
177
178 pub fn max_bytes(mut self, max: u64) -> Self {
180 self.max_bytes = Some(max);
181 self
182 }
183
184 pub fn build(self) -> Result<Datastore> {
185 let root = match self.cache_root {
186 Some(root) => root,
187 None => config::default_cache_root()?,
188 };
189 let layout = Layout::new(root);
190 layout.ensure()?;
191 let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
192 let progress = match (self.on_progress, self.progress) {
193 (Some(callback), _) => Progress::Callback(callback),
194 (None, Some(false)) => Progress::Off,
195 (None, _) => Progress::Bars,
196 };
197 let provider: Option<Arc<dyn CredentialProvider>> = self.credentials.map(Arc::from);
198 let fetcher = Fetcher::new(timeout, provider, progress.clone(), true)?;
199 let mirror = match self.mirror {
200 Some(m) => Some(mirror::open(&m, timeout, progress)?),
201 None => None,
202 };
203 Ok(Datastore {
204 layout,
205 mirror,
206 fetcher,
207 allow_upstream: self.allow_upstream.unwrap_or(false),
208 offline: self.offline.unwrap_or(false),
209 max_bytes: self.max_bytes,
210 })
211 }
212}
213
214impl Datastore {
215 pub fn builder() -> DatastoreBuilder {
216 DatastoreBuilder::default()
217 }
218
219 pub fn from_env() -> Result<Self> {
221 DatastoreBuilder::from_env()?.build()
222 }
223
224 pub fn cache_root(&self) -> &Path {
225 self.layout.root()
226 }
227
228 pub fn max_bytes(&self) -> Option<u64> {
230 self.max_bytes
231 }
232
233 pub fn get(&self, artifact: &Artifact) -> Result<PathBuf> {
235 self.get_with_outcome(artifact).map(|(path, _)| path)
236 }
237
238 pub fn get_with_outcome(&self, artifact: &Artifact) -> Result<(PathBuf, ResolveOutcome)> {
240 self.get_with_budget(artifact, None)
241 }
242
243 #[cfg(feature = "server")]
244 pub(crate) fn get_limited(&self, artifact: &Artifact, bytes: u64) -> Result<PathBuf> {
245 self.get_with_budget(artifact, Some(&Cell::new(bytes)))
246 .map(|(path, _)| path)
247 }
248
249 fn get_with_budget(
250 &self,
251 artifact: &Artifact,
252 budget: Option<&Cell<u64>>,
253 ) -> Result<(PathBuf, ResolveOutcome)> {
254 let start = Instant::now();
255 let key = &artifact.key;
256 let _lock = self.layout.lock(key)?;
257
258 if let Some(entry) = self.layout.read_entry(key)? {
259 if let Some(path) = self.local_hit(artifact, &entry)? {
260 if budget.is_some_and(|budget| entry.bytes > budget.get()) {
261 return Err(DatastoreError::TransferLimit);
262 }
263 return Ok((path, outcome(Layer::LocalDisk, entry.bytes, start, None)));
264 }
265 }
266 if self.offline {
267 return Err(DatastoreError::OfflineMiss { key: key.clone() });
268 }
269
270 let mirror_reason = match &self.mirror {
271 None => "no mirror configured".to_string(),
272 Some(mirror) => match self.fetch_from_mirror(artifact, mirror.as_ref(), budget) {
273 Ok(Some((path, bytes))) => {
274 return Ok((path, outcome(Layer::Mirror, bytes, start, None)));
275 }
276 Ok(None) => format!("{} has no entry", mirror.location(key)),
277 Err(e @ DatastoreError::ContentRejected { .. }) => return Err(e),
278 Err(e @ DatastoreError::TransferLimit) => return Err(e),
279 Err(e) => e.to_string(),
280 },
281 };
282 if artifact.sources.is_empty() {
283 return Err(DatastoreError::ManualRequired {
284 key: key.clone(),
285 instructions: artifact.provenance.description.clone(),
286 });
287 }
288 if !self.allow_upstream {
289 return Err(DatastoreError::MirrorUnreachable {
290 key: key.clone(),
291 reason: mirror_reason,
292 });
293 }
294
295 let mut attempts = Vec::with_capacity(artifact.sources.len());
296 for (index, source) in artifact.sources.iter().enumerate() {
297 match self.fetch_from_upstream(artifact, index, budget) {
298 Ok((path, bytes)) => {
299 return Ok((path, outcome(Layer::Upstream, bytes, start, Some(index))));
300 }
301 Err(e @ DatastoreError::TransferLimit) => return Err(e),
302 Err(
303 e @ (DatastoreError::ContentRejected { .. }
304 | DatastoreError::NoCredential { .. }
305 | DatastoreError::CredentialRejected { .. }),
306 ) if artifact.sources.len() == 1 => {
307 return Err(e);
308 }
309 Err(e) => attempts.push(format!("{}: {e}", sanitize(&source.url))),
310 }
311 }
312 Err(DatastoreError::AllSourcesFailed {
313 key: key.clone(),
314 attempts,
315 })
316 }
317
318 pub fn get_bytes(&self, artifact: &Artifact) -> Result<Vec<u8>> {
320 Ok(std::fs::read(self.get(artifact)?)?)
321 }
322
323 pub fn import(&self, artifact: &Artifact, path: &Path) -> Result<PathBuf> {
326 let _lock = self.layout.lock(&artifact.key)?;
327 let mut file = std::fs::File::open(path)?;
328 let (tmp, digest, bytes) = self
329 .receive(artifact, None, |sink| {
330 std::io::copy(&mut file, sink)?;
331 Ok(true)
332 })?
333 .expect("a local file is always served");
334 let mut entry = IndexEntry::new(digest, bytes, Layer::LocalDisk);
335 entry.source = Some("local:import".into());
336 self.publish(artifact, tmp, entry)
337 }
338
339 pub fn peek(&self, key: &ArtifactKey) -> Option<PathBuf> {
341 let entry = self.layout.read_entry(key).ok()??;
342 let path = self.layout.blob_path(&entry.digest);
343 path.is_file().then_some(path)
344 }
345
346 pub fn contains(&self, key: &ArtifactKey) -> bool {
347 self.peek(key).is_some()
348 }
349
350 pub fn entry(&self, key: &ArtifactKey) -> Option<IndexEntry> {
352 let entry = self.layout.read_entry(key).ok()??;
353 self.layout
354 .blob_path(&entry.digest)
355 .is_file()
356 .then_some(entry)
357 }
358
359 pub fn remove(&self, key: &ArtifactKey) -> Result<()> {
360 let _lock = self.layout.lock(key)?;
361 let _store = self.layout.store_lock()?;
362 let Some(entry) = self.layout.read_entry(key)? else {
363 return Ok(());
364 };
365 self.layout.remove_entry(key)?;
366 if !self.digest_referenced(&entry.digest)? {
367 self.layout.remove_blob(&entry.digest)?;
368 }
369 Ok(())
370 }
371
372 pub fn keys(&self) -> Result<Vec<ArtifactKey>> {
373 self.layout.keys()
374 }
375
376 pub fn total_bytes(&self) -> Result<u64> {
379 Ok(self.layout.blobs()?.iter().map(|(_, len)| len).sum())
380 }
381
382 pub fn verify(&self) -> Result<Vec<VerifyFailure>> {
387 let mut failures = Vec::new();
388 for (key, entry) in self.entries()? {
389 let path = self.layout.blob_path(&entry.digest);
390 if !path.is_file() {
391 failures.push(VerifyFailure {
392 key,
393 reason: format!("blob {} is missing", entry.digest),
394 expected: entry.digest,
395 actual: None,
396 });
397 continue;
398 }
399 let length = std::fs::metadata(&path)?.len();
400 let actual = digest_file(&path)?;
401 let reason = if actual != entry.digest {
402 format!("blob hashes to {actual}, index records {}", entry.digest)
403 } else if length != entry.bytes {
404 format!("blob is {length} bytes, index records {}", entry.bytes)
405 } else {
406 continue;
407 };
408 failures.push(VerifyFailure {
409 key,
410 expected: entry.digest,
411 actual: Some(actual),
412 reason,
413 });
414 }
415 Ok(failures)
416 }
417
418 pub fn gc(&self, max_bytes: u64) -> Result<Vec<ArtifactKey>> {
425 let (mut entries, sizes) = {
426 let _store = self.layout.store_lock()?;
427 let entries = self.entries()?;
428 let referenced: HashSet<&str> =
429 entries.iter().map(|(_, e)| e.digest.as_str()).collect();
430 let mut sizes: HashMap<String, u64> = HashMap::new();
431 for (digest, len) in self.layout.blobs()? {
432 if referenced.contains(digest.as_str()) {
433 sizes.insert(digest, len);
434 } else {
435 self.layout.remove_blob(&digest)?;
436 }
437 }
438 (entries, sizes)
439 };
440 entries.sort_by_key(|(_, e)| e.fetched_at);
441 let mut total: u64 = sizes.values().sum();
442 let mut removed = Vec::new();
443 for (key, snapshot) in &entries {
444 if total <= max_bytes {
445 break;
446 }
447 let _lock = self.layout.lock(key)?;
448 let _store = self.layout.store_lock()?;
449 let Some(entry) = self.layout.read_entry(key)? else {
452 continue;
453 };
454 if entry.digest != snapshot.digest {
455 continue;
456 }
457 self.layout.remove_entry(key)?;
458 if !self.digest_referenced(&entry.digest)? {
459 self.layout.remove_blob(&entry.digest)?;
460 total = total.saturating_sub(sizes.get(&entry.digest).copied().unwrap_or(0));
461 }
462 removed.push(key.clone());
463 }
464 Ok(removed)
465 }
466
467 #[cfg(feature = "server")]
470 pub(crate) fn blob_bytes(&self) -> Result<u64> {
471 let _store = self.layout.store_lock()?;
472 self.layout
473 .blobs()?
474 .iter()
475 .try_fold(0u64, |total, (_, bytes)| {
476 total
477 .checked_add(*bytes)
478 .ok_or_else(|| DatastoreError::Config("cache byte count overflow".into()))
479 })
480 }
481
482 fn entries(&self) -> Result<Vec<(ArtifactKey, IndexEntry)>> {
483 let mut out = Vec::new();
484 for key in self.layout.keys()? {
485 if let Some(entry) = self.layout.read_entry(&key)? {
486 out.push((key, entry));
487 }
488 }
489 Ok(out)
490 }
491
492 fn digest_referenced(&self, digest: &str) -> Result<bool> {
493 Ok(self.entries()?.iter().any(|(_, e)| e.digest == digest))
494 }
495
496 fn local_hit(&self, artifact: &Artifact, entry: &IndexEntry) -> Result<Option<PathBuf>> {
500 let path = self.layout.blob_path(&entry.digest);
501 if !path.is_file() {
502 return Ok(None);
503 }
504 if pinned_sha256s(&artifact.check)
505 .iter()
506 .any(|pin| *pin != entry.digest)
507 {
508 return Ok(None);
509 }
510 let corrupt = |what: &str, got: String| DatastoreError::ContentRejected {
511 key: artifact.key.clone(),
512 failure: CheckFailure {
513 check: format!("cached blob {what}"),
514 got: format!(
515 "{got}; the blob is corrupt — run verify and remove the keys it reports"
516 ),
517 },
518 };
519 let length = std::fs::metadata(&path)?.len();
520 if length != entry.bytes {
521 return Err(corrupt(
522 "size",
523 format!("{length} bytes, but the index records {}", entry.bytes),
524 ));
525 }
526 let actual = digest_file(&path)?;
527 if actual != entry.digest {
528 return Err(corrupt(
529 "digest",
530 format!("{actual}, but the index records {}", entry.digest),
531 ));
532 }
533 validate(artifact, &path, entry.bytes, &actual)?;
534 Ok(Some(path))
535 }
536
537 fn fetch_from_mirror(
538 &self,
539 artifact: &Artifact,
540 mirror: &dyn MirrorRead,
541 budget: Option<&Cell<u64>>,
542 ) -> Result<Option<(PathBuf, u64)>> {
543 let Some((tmp, digest, bytes)) =
544 self.receive(artifact, budget, |sink| mirror.fetch(&artifact.key, sink))?
545 else {
546 return Ok(None);
547 };
548 let mut entry = IndexEntry::new(digest, bytes, Layer::Mirror);
549 entry.source = Some(mirror.location(&artifact.key));
550 let path = self.publish(artifact, tmp, entry)?;
551 Ok(Some((path, bytes)))
552 }
553
554 fn fetch_from_upstream(
555 &self,
556 artifact: &Artifact,
557 index: usize,
558 budget: Option<&Cell<u64>>,
559 ) -> Result<(PathBuf, u64)> {
560 let source = &artifact.sources[index];
561 let mut download = None;
562 let received = self.receive(artifact, budget, |sink| {
563 download = self.fetcher.request(source, sink)?;
564 Ok(download.is_some())
565 })?;
566 let (Some((tmp, digest, bytes)), Some(download)) = (received, download) else {
567 return Err(DatastoreError::Mirror("HTTP 404 Not Found".into()));
568 };
569 let mut entry = IndexEntry::new(digest, bytes, Layer::Upstream);
570 entry.source = Some(sanitize(&source.url));
571 entry.etag = download.etag;
572 entry.provider_identity = download.provider_identity;
573 let path = self.publish(artifact, tmp, entry)?;
574 Ok((path, bytes))
575 }
576
577 fn receive<F>(
582 &self,
583 artifact: &Artifact,
584 budget: Option<&Cell<u64>>,
585 producer: F,
586 ) -> Result<Option<(NamedTempFile, String, u64)>>
587 where
588 F: FnOnce(&mut dyn Write) -> Result<bool>,
589 {
590 let mut tmp = self.layout.tmp_file()?;
591 let mut writer = Receiver::new(tmp.as_file_mut(), &artifact.check);
592 writer.budget = budget;
593 let result = producer(&mut writer);
594 if writer.limit_exceeded {
595 return Err(DatastoreError::TransferLimit);
596 }
597 let served = match result {
598 Ok(served) => served,
599 Err(DatastoreError::Io(_)) if writer.rejected.is_some() => false,
600 Err(e) => return Err(e),
601 };
602 if let Some(failure) = writer.rejected.take() {
603 return Err(DatastoreError::ContentRejected {
604 key: artifact.key.clone(),
605 failure,
606 });
607 }
608 if !served {
609 return Ok(None);
610 }
611 writer
612 .finish_prefix()
613 .map_err(|failure| DatastoreError::ContentRejected {
614 key: artifact.key.clone(),
615 failure,
616 })?;
617 let (digest, bytes) = writer.finish();
618 tmp.as_file_mut().flush()?;
619 Ok(Some((tmp, digest, bytes)))
620 }
621
622 fn publish(
627 &self,
628 artifact: &Artifact,
629 tmp: NamedTempFile,
630 entry: IndexEntry,
631 ) -> Result<PathBuf> {
632 validate(artifact, tmp.path(), entry.bytes, &entry.digest)?;
633 let _store = self.layout.store_lock()?;
634 let path = self
635 .layout
636 .publish(tmp, &entry.digest)?
637 .ok_or_else(|| DatastoreError::ContentRejected {
638 key: artifact.key.clone(),
639 failure: CheckFailure {
640 check: "cached blob digest".into(),
641 got: format!(
642 "an existing blob at {} no longer hashes to its address; run verify and remove the keys it reports",
643 entry.digest
644 ),
645 },
646 })?;
647 self.layout.write_entry(&artifact.key, &entry)?;
648 Ok(path)
649 }
650}
651
652fn validate(artifact: &Artifact, path: &Path, bytes: u64, digest: &str) -> Result<()> {
656 let reject = |check: &str, got: String| DatastoreError::ContentRejected {
657 key: artifact.key.clone(),
658 failure: CheckFailure {
659 check: check.into(),
660 got,
661 },
662 };
663 if let Some(expected) = artifact.expected_bytes {
664 if expected != bytes {
665 return Err(reject(
666 "expected_bytes",
667 format!("{bytes} bytes, expected {expected}"),
668 ));
669 }
670 }
671 for pin in pinned_sha256s(&artifact.check) {
672 if pin != digest {
673 return Err(reject("Sha256", format!("digest {digest}, expected {pin}")));
674 }
675 }
676 without_sha256(&artifact.check)
677 .check_file(path)
678 .map_err(|failure| DatastoreError::ContentRejected {
679 key: artifact.key.clone(),
680 failure,
681 })
682}
683
684fn pinned_sha256s(check: &ContentCheck) -> Vec<&str> {
687 match check {
688 ContentCheck::Sha256(digest) => vec![digest],
689 ContentCheck::All(checks) => checks.iter().flat_map(pinned_sha256s).collect(),
690 _ => Vec::new(),
691 }
692}
693
694fn without_sha256(check: &ContentCheck) -> ContentCheck {
695 match check {
696 ContentCheck::Sha256(_) => ContentCheck::None,
697 ContentCheck::All(checks) => ContentCheck::All(checks.iter().map(without_sha256).collect()),
698 other => other.clone(),
699 }
700}
701
702fn outcome(
703 layer: Layer,
704 bytes: u64,
705 start: Instant,
706 source_index: Option<usize>,
707) -> ResolveOutcome {
708 ResolveOutcome {
709 layer,
710 bytes,
711 duration: start.elapsed(),
712 source_index,
713 }
714}
715
716struct Receiver<'a> {
719 inner: &'a mut std::fs::File,
720 check: &'a ContentCheck,
721 hasher: Sha256,
722 bytes: u64,
723 head: Vec<u8>,
724 prefix_checked: bool,
725 rejected: Option<CheckFailure>,
726 budget: Option<&'a Cell<u64>>,
727 limit_exceeded: bool,
728}
729
730impl<'a> Receiver<'a> {
731 fn new(inner: &'a mut std::fs::File, check: &'a ContentCheck) -> Self {
732 Self {
733 inner,
734 check,
735 hasher: Sha256::new(),
736 bytes: 0,
737 head: Vec::with_capacity(PREFIX_BYTES),
738 prefix_checked: false,
739 rejected: None,
740 budget: None,
741 limit_exceeded: false,
742 }
743 }
744
745 fn check_prefix(&mut self) -> std::io::Result<()> {
746 self.prefix_checked = true;
747 if let Err(failure) = self.check.check_prefix(&self.head) {
748 let message = format!("{}: {}", failure.check, failure.got);
749 self.rejected = Some(failure);
750 return Err(std::io::Error::other(message));
751 }
752 Ok(())
753 }
754
755 fn finish_prefix(&mut self) -> std::result::Result<(), CheckFailure> {
757 if !self.prefix_checked {
758 self.prefix_checked = true;
759 self.check.check_prefix(&self.head)?;
760 }
761 Ok(())
762 }
763
764 fn finish(self) -> (String, u64) {
765 (format!("{:x}", self.hasher.finalize()), self.bytes)
766 }
767}
768
769impl Write for Receiver<'_> {
770 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
771 if let Some(budget) = self.budget {
772 let remaining = budget.get().checked_sub(buf.len() as u64);
773 if self.limit_exceeded || remaining.is_none() {
774 self.limit_exceeded = true;
775 return Err(std::io::Error::other("fill byte reservation exhausted"));
776 }
777 budget.set(remaining.unwrap());
780 }
781 if let Some(failure) = &self.rejected {
782 return Err(std::io::Error::other(format!(
783 "{}: {}",
784 failure.check, failure.got
785 )));
786 }
787 if !self.prefix_checked {
788 let take = (PREFIX_BYTES - self.head.len()).min(buf.len());
789 self.head.extend_from_slice(&buf[..take]);
790 if self.head.len() == PREFIX_BYTES {
791 self.check_prefix()?;
792 }
793 }
794 self.inner.write_all(buf)?;
795 self.hasher.update(buf);
796 self.bytes += buf.len() as u64;
797 Ok(buf.len())
798 }
799
800 fn flush(&mut self) -> std::io::Result<()> {
801 self.inner.flush()
802 }
803}
804
805impl IndexEntry {
806 fn new(digest: String, bytes: u64, layer: Layer) -> Self {
807 Self {
808 digest,
809 bytes,
810 fetched_at: now_unix(),
811 source: None,
812 etag: None,
813 provider_identity: None,
814 layer,
815 }
816 }
817}
818
819#[cfg(all(test, feature = "server"))]
820mod limited_transfer_tests {
821 use super::*;
822
823 #[test]
824 fn cumulative_streaming_limit_covers_unknown_sizes_retries_and_wrapped_errors() {
825 let root = tempfile::tempdir().unwrap();
826 let store = Datastore::builder()
827 .cache_root(root.path().to_owned())
828 .build()
829 .unwrap();
830 let artifact = Artifact::new(ArtifactKey::new("large").unwrap(), vec![])
831 .with_check(ContentCheck::None);
832 let budget = Cell::new(10);
833 let first = store.receive(&artifact, Some(&budget), |sink| {
834 sink.write_all(b"123456")?;
835 Err(DatastoreError::Mirror("failed transfer".into()))
836 });
837 assert!(first.is_err());
838 assert_eq!(budget.get(), 4, "retries do not refund materialized bytes");
839 let second = store.receive(&artifact, Some(&budget), |sink| {
840 sink.write_all(b"789")?;
841 assert!(sink.write_all(b"xx").is_err());
842 Err(DatastoreError::Mirror("wrapped write failure".into()))
844 });
845 assert!(matches!(second, Err(DatastoreError::TransferLimit)));
846 assert_eq!(budget.get(), 1);
847 assert_eq!(
848 std::fs::read_dir(root.path().join("tmp")).unwrap().count(),
849 0
850 );
851 assert_eq!(store.blob_bytes().unwrap(), 0);
852 let unbounded = store
853 .receive(&artifact, None, |sink| {
854 sink.write_all(&[0; 32])?;
855 Ok(true)
856 })
857 .unwrap()
858 .unwrap();
859 assert_eq!(unbounded.2, 32);
860 }
861
862 #[test]
863 fn local_hits_obey_fill_limit_but_normal_get_does_not_evict() {
864 let root = tempfile::tempdir().unwrap();
865 let store = Datastore::builder()
866 .cache_root(root.path().to_owned())
867 .max_bytes(0)
868 .build()
869 .unwrap();
870 let artifact = Artifact::new(ArtifactKey::new("existing").unwrap(), vec![])
871 .with_check(ContentCheck::None);
872 let input = tempfile::NamedTempFile::new().unwrap();
873 std::fs::write(input.path(), b"payload").unwrap();
874 let path = store.import(&artifact, input.path()).unwrap();
875 assert!(matches!(
876 store.get_limited(&artifact, 6),
877 Err(DatastoreError::TransferLimit)
878 ));
879 assert_eq!(store.get(&artifact).unwrap(), path);
880 assert!(path.exists());
881 }
882}