1use std::fs::{self, File};
30use std::io::{self, Read, Write};
31use std::path::{Path, PathBuf};
32use std::time::{SystemTime, UNIX_EPOCH};
33
34use serde::{Deserialize, Serialize};
35use serde_json;
36
37const HASH_PREFIX_LEN: usize = 4;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CacheStatus {
50 NotFound,
52
53 Found,
55
56 Created,
58
59 Copying,
61
62 CopyStalled,
64
65 Pending,
67
68 Removed,
70
71 Skipped,
73}
74
75impl CacheStatus {
76 pub fn description(&self) -> &'static str {
78 match self {
79 CacheStatus::NotFound => "was not found",
80 CacheStatus::Found => "was found",
81 CacheStatus::Created => "was created",
82 CacheStatus::Copying => "payload is still being copied to cache",
83 CacheStatus::CopyStalled => {
84 "payload copy has stalled (see docs for cleaning instructions)"
85 }
86 CacheStatus::Pending => "is pending caching",
87 CacheStatus::Removed => "was deleted",
88 CacheStatus::Skipped => "is not being cached due to cache size limit",
89 }
90 }
91}
92
93#[derive(Debug, thiserror::Error)]
97pub enum PackageCacheError {
98 #[error("Not a directory: {0}")]
99 NotADirectory(PathBuf),
100
101 #[error("Package is not cacheable: {0}")]
102 NotCacheable(String),
103
104 #[error("Variant root not on disk: {0}")]
105 VariantRootNotOnDisk(String),
106
107 #[error("Disk full: cannot cache variant (need {needed} bytes, have {available})")]
108 DiskFull { needed: u64, available: u64 },
109
110 #[error("Cache path error: {0}")]
111 PathError(String),
112
113 #[error("IO error: {0}")]
114 Io(#[from] io::Error),
115
116 #[error("JSON error: {0}")]
117 Json(#[from] serde_json::Error),
118
119 #[error("Lock timeout: could not acquire lock on {0}")]
120 LockTimeout(PathBuf),
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct VariantHandle {
130 pub name: String,
132
133 pub version: Option<String>,
135
136 pub index: Option<usize>,
138
139 pub attributes: std::collections::HashMap<String, String>,
141}
142
143impl VariantHandle {
144 pub fn new(name: String, version: Option<String>, index: Option<usize>) -> Self {
146 Self {
147 name,
148 version,
149 index,
150 attributes: std::collections::HashMap::new(),
151 }
152 }
153
154 fn hashable_repr(&self) -> String {
156 let mut s = format!("name={}", self.name);
157 if let Some(v) = &self.version {
158 s.push_str(&format!(", version={}", v));
159 }
160 if let Some(i) = self.index {
161 s.push_str(&format!(", index={}", i));
162 }
163 let mut attrs: Vec<_> = self.attributes.iter().collect();
165 attrs.sort_by_key(|(k, _)| *k);
166 for (k, v) in attrs {
167 s.push_str(&format!(", {}={}", k, v));
168 }
169 s
170 }
171
172 pub fn sha1_hash(&self) -> String {
174 use sha1::{Digest, Sha1};
175 let mut hasher = Sha1::new();
176 hasher.update(self.hashable_repr().as_bytes());
177 hasher
178 .finalize()
179 .iter()
180 .map(|byte| format!("{byte:02x}"))
181 .collect::<Vec<_>>()
182 .join("")
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CachedVariantInfo {
191 pub handle: VariantHandle,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub created_at: Option<u64>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub last_accessed: Option<u64>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub payload_size: Option<u64>,
205}
206
207pub struct PackageCache {
222 root: PathBuf,
224
225 config: CacheConfig,
227}
228
229#[derive(Debug, Clone)]
231pub struct CacheConfig {
232 pub max_size_bytes: Option<u64>,
234
235 pub min_free_space_bytes: u64,
237
238 pub max_age_secs: Option<u64>,
240
241 pub cache_local: bool,
243}
244
245impl Default for CacheConfig {
246 fn default() -> Self {
247 Self {
248 max_size_bytes: None,
249 min_free_space_bytes: 100 * 1024 * 1024, max_age_secs: None,
251 cache_local: true,
252 }
253 }
254}
255
256impl PackageCache {
257 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, PackageCacheError> {
263 let root = path.as_ref().to_path_buf();
264
265 if !root.is_dir() {
266 return Err(PackageCacheError::NotADirectory(root));
267 }
268
269 let sys_dir = root.join(".sys");
271 fs::create_dir_all(&sys_dir)?;
272 fs::create_dir_all(sys_dir.join("pending"))?;
273 fs::create_dir_all(sys_dir.join("to_delete"))?;
274 fs::create_dir_all(sys_dir.join("log"))?;
275
276 Ok(Self {
277 root,
278 config: CacheConfig::default(),
279 })
280 }
281
282 pub fn with_config<P: AsRef<Path>>(
284 path: P,
285 config: CacheConfig,
286 ) -> Result<Self, PackageCacheError> {
287 let mut cache = Self::new(path)?;
288 cache.config = config;
289 Ok(cache)
290 }
291
292 pub fn root(&self) -> &Path {
294 &self.root
295 }
296
297 pub fn config(&self) -> &CacheConfig {
299 &self.config
300 }
301
302 fn hash_path(&self, handle: &VariantHandle) -> PathBuf {
310 let version_str = handle.version.as_deref().unwrap_or("_NO_VERSION");
311 let hash = handle.sha1_hash();
312 let hash_prefix = &hash[..HASH_PREFIX_LEN.min(hash.len())];
313 let name_lower = handle.name.to_lowercase();
315 let version_lower = version_str.to_lowercase();
316 self.root
317 .join(&name_lower)
318 .join(&version_lower)
319 .join(hash_prefix)
320 }
321
322 fn sys_dir(&self) -> PathBuf {
324 self.root.join(".sys")
325 }
326
327 fn to_delete_dir(&self) -> PathBuf {
329 self.sys_dir().join("to_delete")
330 }
331
332 pub fn get_cached_root(&self, handle: &VariantHandle) -> (CacheStatus, Option<PathBuf>) {
342 let hash_path = self.hash_path(handle);
343
344 if !hash_path.is_dir() {
345 return (CacheStatus::NotFound, None);
346 }
347
348 let entries = match fs::read_dir(&hash_path) {
350 Ok(entries) => entries,
351 Err(_) => return (CacheStatus::NotFound, None),
352 };
353
354 for entry in entries.flatten() {
355 let path = entry.path();
356
357 if path.extension().and_then(|s| s.to_str()) == Some("json") {
359 let json_path = path.clone();
360 let payload_path = path.with_extension(""); let metadata = match Self::read_metadata(&json_path) {
364 Ok(m) => m,
365 Err(_) => continue, };
367
368 if metadata.handle.hashable_repr() == handle.hashable_repr() {
369 let copying_flag = json_path.with_file_name(format!(
371 ".copying-{}",
372 payload_path.file_name().unwrap().to_string_lossy()
373 ));
374
375 if copying_flag.is_file() {
376 if Self::is_file_stalled(©ing_flag) {
378 return (CacheStatus::CopyStalled, Some(payload_path));
379 }
380 return (CacheStatus::Copying, Some(payload_path));
381 }
382
383 let _ = Self::update_access_time(&json_path);
385
386 return (CacheStatus::Found, Some(payload_path));
387 }
388 }
389 }
390
391 (CacheStatus::NotFound, None)
392 }
393
394 pub fn add_variant(
408 &self,
409 handle: &VariantHandle,
410 source_root: &Path,
411 force: bool,
412 ) -> Result<(CacheStatus, PathBuf), PackageCacheError> {
413 if !source_root.is_dir() {
414 return Err(PackageCacheError::VariantRootNotOnDisk(
415 source_root.display().to_string(),
416 ));
417 }
418
419 let (status, cached_path) = self.get_cached_root(handle);
421 match status {
422 CacheStatus::Found | CacheStatus::CopyStalled => {
423 if let Some(path) = cached_path {
424 return Ok((status, path));
425 }
426 }
427 CacheStatus::Copying => {
428 if let Some(path) = cached_path {
430 return Ok((status, path));
431 }
432 }
433 _ => {}
434 }
435
436 if !force {
438 let source_size = Self::directory_size(source_root)?;
439 if !self.check_disk_space(source_size)? {
440 return Ok((CacheStatus::Skipped, self.hash_path(handle)));
441 }
442 }
443
444 let hash_path = self.hash_path(handle);
446 fs::create_dir_all(&hash_path)?;
447
448 let increment = Self::next_increment(&hash_path)?;
450
451 let payload_path = hash_path.join(&increment);
452 let json_path = hash_path.join(format!("{}.json", increment));
453 let copying_flag = hash_path.join(format!(".copying-{}", increment));
454
455 File::create(©ing_flag)?;
457
458 let now = SystemTime::now()
460 .duration_since(UNIX_EPOCH)
461 .unwrap()
462 .as_secs();
463 let source_size = Self::directory_size(source_root)?;
464 let metadata = CachedVariantInfo {
465 handle: handle.clone(),
466 created_at: Some(now),
467 last_accessed: Some(now),
468 payload_size: Some(source_size),
469 };
470
471 let json_str = serde_json::to_string_pretty(&metadata)?;
473 File::create(&json_path)?.write_all(json_str.as_bytes())?;
474
475 Self::copy_dir_recursive(source_root, &payload_path)?;
477
478 let _ = fs::remove_file(copying_flag);
480
481 Ok((CacheStatus::Created, payload_path))
482 }
483
484 pub fn remove_variant(&self, handle: &VariantHandle) -> (CacheStatus, Option<PathBuf>) {
489 let (status, cached_path) = self.get_cached_root(handle);
490
491 match status {
492 CacheStatus::NotFound => (CacheStatus::NotFound, None),
493 CacheStatus::Copying | CacheStatus::CopyStalled => {
494 (status, cached_path)
496 }
497 CacheStatus::Found => {
498 if let Some(ref path) = cached_path {
499 let dest = self.to_delete_dir().join(format!(
500 "{}-{}",
501 handle.name,
502 uuid::Uuid::new_v4()
503 ));
504
505 if fs::rename(path, &dest).is_err() {
507 let _ = Self::copy_dir_recursive(path, &dest);
509 let _ = fs::remove_dir_all(path);
510 }
511
512 let json_path = path.with_extension("json");
514 let _ = fs::remove_file(json_path);
515
516 Self::cleanup_empty_dirs(path);
518 }
519 (CacheStatus::Removed, cached_path)
520 }
521 _ => (status, cached_path),
522 }
523 }
524
525 pub fn list_cached(&self) -> Vec<(VariantHandle, PathBuf, CacheStatus)> {
529 let mut results = Vec::new();
530
531 if let Ok(pkg_entries) = fs::read_dir(&self.root) {
532 for pkg_entry in pkg_entries.flatten() {
533 let pkg_path = pkg_entry.path();
534 if !pkg_path.is_dir()
535 || pkg_path
536 .file_name()
537 .unwrap()
538 .to_string_lossy()
539 .starts_with('.')
540 {
541 continue;
542 }
543
544 if let Ok(ver_entries) = fs::read_dir(&pkg_path) {
545 for ver_entry in ver_entries.flatten() {
546 let ver_path = ver_entry.path();
547 if !ver_path.is_dir() {
548 continue;
549 }
550
551 if let Ok(hash_entries) = fs::read_dir(&ver_path) {
552 for hash_entry in hash_entries.flatten() {
553 let hash_path = hash_entry.path();
554 if !hash_path.is_dir() {
555 continue;
556 }
557
558 if let Ok(meta_entries) = fs::read_dir(&hash_path) {
560 for meta_entry in meta_entries.flatten() {
561 let meta_path = meta_entry.path();
562 if meta_path.extension().and_then(|s| s.to_str())
563 == Some("json")
564 && let Ok(metadata) = Self::read_metadata(&meta_path)
565 {
566 let payload_path = meta_path.with_extension("");
567 let status = if payload_path.is_dir() {
568 CacheStatus::Found
569 } else {
570 CacheStatus::Pending
571 };
572 results.push((metadata.handle, payload_path, status));
573 }
574 }
575 }
576 }
577 }
578 }
579 }
580 }
581 }
582
583 results
584 }
585
586 pub fn clean(&self, time_limit_secs: Option<u64>) -> CleanStats {
595 let start = SystemTime::now();
596 let mut stats = CleanStats::default();
597
598 let to_delete = self.to_delete_dir();
600 if let Ok(entries) = fs::read_dir(&to_delete) {
601 for entry in entries.flatten() {
602 if Self::check_time_limit(start, time_limit_secs) {
603 break;
604 }
605 let path = entry.path();
606 if path.is_dir() && fs::remove_dir_all(&path).is_ok() {
607 stats.deleted_bytes += Self::directory_size(&path).unwrap_or(0);
608 stats.entries_deleted += 1;
609 }
610 }
611 }
612
613 if let Some(max_age) = self.config.max_age_secs {
615 let now = SystemTime::now()
616 .duration_since(UNIX_EPOCH)
617 .unwrap()
618 .as_secs();
619
620 for (handle, path, status) in self.list_cached() {
621 if Self::check_time_limit(start, time_limit_secs) {
622 break;
623 }
624
625 if status != CacheStatus::Found {
626 continue;
627 }
628
629 let json_path = path.with_extension("json");
631 if let Ok(metadata) = Self::read_metadata(&json_path)
632 && let Some(accessed) = metadata.last_accessed
633 && now - accessed > max_age
634 {
635 let _ = self.remove_variant(&handle);
636 stats.entries_deleted += 1;
637 stats.deleted_bytes += metadata.payload_size.unwrap_or(0);
638 }
639 }
640 }
641
642 stats
643 }
644
645 fn read_metadata(path: &Path) -> Result<CachedVariantInfo, PackageCacheError> {
649 let mut file = File::open(path)?;
650 let mut contents = String::new();
651 file.read_to_string(&mut contents)?;
652 Ok(serde_json::from_str(&contents)?)
653 }
654
655 fn update_access_time(json_path: &Path) -> Result<(), PackageCacheError> {
657 let mut metadata: CachedVariantInfo = Self::read_metadata(json_path)?;
658 metadata.last_accessed = Some(
659 SystemTime::now()
660 .duration_since(UNIX_EPOCH)
661 .unwrap()
662 .as_secs(),
663 );
664 let json_str = serde_json::to_string_pretty(&metadata)?;
665 let mut file = File::create(json_path)?;
666 file.write_all(json_str.as_bytes())?;
667 Ok(())
668 }
669
670 fn is_file_stalled(path: &Path) -> bool {
672 if let Ok(metadata) = fs::metadata(path)
673 && let Ok(mtime) = metadata.modified()
674 {
675 let age = SystemTime::now().duration_since(mtime).unwrap_or_default();
676 return age.as_secs() > 300; }
678 false
679 }
680
681 fn next_increment(hash_path: &Path) -> Result<String, PackageCacheError> {
683 let mut max_inc = None;
684
685 if let Ok(entries) = fs::read_dir(hash_path) {
686 for entry in entries.flatten() {
687 let name = entry.file_name().to_string_lossy().to_string();
688 if name.ends_with(".json") {
689 let inc = name.trim_end_matches(".json");
690 match &max_inc {
691 None => max_inc = Some(inc.to_string()),
692 Some(current) => {
693 if inc > current.as_str() {
694 max_inc = Some(inc.to_string());
695 }
696 }
697 }
698 }
699 }
700 }
701
702 let next = match max_inc {
703 None => "a".to_string(),
704 Some(ref inc) => increment_string(inc),
705 };
706
707 Ok(next)
708 }
709
710 fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), io::Error> {
712 fs::create_dir_all(dst)?;
713
714 for entry in fs::read_dir(src)? {
715 let entry = entry?;
716 let src_path = entry.path();
717 let dst_path = dst.join(entry.file_name());
718
719 if src_path.is_dir() {
720 Self::copy_dir_recursive(&src_path, &dst_path)?;
721 } else {
722 fs::copy(&src_path, &dst_path)?;
723 }
724 }
725
726 Ok(())
727 }
728
729 fn directory_size(path: &Path) -> Result<u64, io::Error> {
731 let mut total = 0u64;
732
733 #[cfg(unix)]
734 let mut seen_inodes: std::collections::HashSet<(u64, u64)> =
735 std::collections::HashSet::new();
736
737 let mut stack = vec![path.to_path_buf()];
738
739 while let Some(current) = stack.pop() {
740 let entries = match fs::read_dir(¤t) {
741 Ok(e) => e,
742 Err(_) => continue,
743 };
744
745 for entry in entries {
746 let entry = match entry {
747 Ok(e) => e,
748 Err(_) => continue,
749 };
750
751 let path = entry.path();
752 let metadata = match fs::metadata(&path) {
753 Ok(m) => m,
754 Err(_) => continue,
755 };
756
757 #[cfg(unix)]
759 {
760 use std::os::unix::fs::MetadataExt;
761 let inode = (metadata.dev(), metadata.ino());
762 if !seen_inodes.insert(inode) {
763 continue;
764 }
765 }
766
767 if metadata.is_file() {
768 total += metadata.len();
769 } else if metadata.is_dir() {
770 stack.push(path);
771 }
772 }
773 }
774
775 Ok(total)
776 }
777
778 fn check_disk_space(&self, needed: u64) -> Result<bool, PackageCacheError> {
780 let available = fs2::available_space(&self.root)?;
781 Ok(available - needed > self.config.min_free_space_bytes)
782 }
783
784 pub fn cache_near_full(&self) -> bool {
789 fs2::available_space(&self.root)
790 .map(|available| available < self.config.min_free_space_bytes)
791 .unwrap_or(false) }
793
794 pub fn variant_meets_space_requirements(&self, variant_root: &Path) -> bool {
806 let available = match fs2::available_space(&self.root) {
807 Ok(space) => space,
808 Err(_) => return false, };
810
811 let variant_size = Self::directory_size(variant_root).unwrap_or(0);
812
813 available > variant_size + self.config.min_free_space_bytes
815 }
816
817 fn cleanup_empty_dirs(path: &Path) {
819 let mut current = path.parent();
820 while let Some(dir) = current {
821 if dir.file_name().unwrap().to_string_lossy().starts_with('.') {
822 break;
823 }
824 if fs::read_dir(dir)
825 .map(|mut d| d.next().is_some())
826 .unwrap_or(true)
827 {
828 break;
829 }
830 let _ = fs::remove_dir(dir);
831 current = dir.parent();
832 }
833 }
834
835 fn check_time_limit(start: SystemTime, limit: Option<u64>) -> bool {
837 if let Some(limit) = limit {
838 let elapsed = start.elapsed().unwrap_or_default().as_secs();
839 return elapsed > limit;
840 }
841 false
842 }
843}
844
845fn increment_string(s: &str) -> String {
851 let mut chars: Vec<char> = s.chars().collect();
852 let mut i = chars.len() - 1;
853
854 loop {
855 if chars[i] == 'z' {
856 chars[i] = 'a';
857 if i == 0 {
858 chars.insert(0, 'a');
859 break;
860 }
861 i -= 1;
862 } else {
863 chars[i] = ((chars[i] as u8) + 1) as char;
864 break;
865 }
866 }
867
868 chars.iter().collect()
869}
870
871#[derive(Debug, Default, Clone)]
875pub struct CleanStats {
876 pub entries_deleted: u64,
878
879 pub deleted_bytes: u64,
881}
882
883#[cfg(test)]
886mod tests {
887 use super::*;
888 use tempfile::TempDir;
889
890 fn make_handle(name: &str, version: Option<&str>) -> VariantHandle {
891 VariantHandle::new(name.to_string(), version.map(String::from), None)
892 }
893
894 #[test]
895 fn test_cache_creation() {
896 let tmp = TempDir::new().unwrap();
897 let cache = PackageCache::new(tmp.path()).unwrap();
898 assert_eq!(cache.root(), tmp.path());
899 }
900
901 #[test]
902 fn test_cache_creation_not_a_dir() {
903 let tmp = TempDir::new().unwrap();
904 let path = tmp.path().join("nonexistent");
905 let result = PackageCache::new(&path);
906 assert!(result.is_err());
907 }
908
909 #[test]
910 fn test_variant_handle_hash() {
911 let h1 = make_handle("python", Some("3.9.0"));
912 let h2 = make_handle("python", Some("3.9.0"));
913 assert_eq!(h1.sha1_hash(), h2.sha1_hash());
914 }
915
916 #[test]
917 fn test_variant_handle_hash_different() {
918 let h1 = make_handle("python", Some("3.9.0"));
919 let h2 = make_handle("python", Some("3.10.0"));
920 assert_ne!(h1.sha1_hash(), h2.sha1_hash());
921 }
922
923 #[test]
924 fn test_add_and_get_variant() {
925 let tmp = TempDir::new().unwrap();
926 let cache = PackageCache::new(tmp.path()).unwrap();
927
928 let payload = tmp.path().join("payload");
930 fs::create_dir_all(&payload).unwrap();
931 fs::write(payload.join("file.txt"), b"hello").unwrap();
932
933 let handle = make_handle("mypkg", Some("1.0.0"));
934 let (status, path) = cache.add_variant(&handle, &payload, false).unwrap();
935
936 assert_eq!(status, CacheStatus::Created);
937 assert!(path.is_dir());
938 }
939
940 #[test]
941 fn test_get_cached_root_found() {
942 let tmp = TempDir::new().unwrap();
943 let cache = PackageCache::new(tmp.path()).unwrap();
944
945 let payload = tmp.path().join("payload");
946 fs::create_dir_all(&payload).unwrap();
947 fs::write(payload.join("file.txt"), b"hello").unwrap();
948
949 let handle = make_handle("mypkg", Some("1.0.0"));
950 cache.add_variant(&handle, &payload, false).unwrap();
951
952 let (status, path) = cache.get_cached_root(&handle);
953 assert_eq!(status, CacheStatus::Found);
954 assert!(path.is_some());
955 }
956
957 #[test]
958 fn test_get_cached_root_not_found() {
959 let tmp = TempDir::new().unwrap();
960 let cache = PackageCache::new(tmp.path()).unwrap();
961
962 let handle = make_handle("nonexistent", Some("1.0.0"));
963 let (status, path) = cache.get_cached_root(&handle);
964 assert_eq!(status, CacheStatus::NotFound);
965 assert!(path.is_none());
966 }
967
968 #[test]
969 fn test_remove_variant() {
970 let tmp = TempDir::new().unwrap();
971 let cache = PackageCache::new(tmp.path()).unwrap();
972
973 let payload = tmp.path().join("payload");
974 fs::create_dir_all(&payload).unwrap();
975 fs::write(payload.join("file.txt"), b"hello").unwrap();
976
977 let handle = make_handle("mypkg", Some("1.0.0"));
978 cache.add_variant(&handle, &payload, false).unwrap();
979
980 let (status, _) = cache.remove_variant(&handle);
981 assert_eq!(status, CacheStatus::Removed);
982
983 let (status, _) = cache.get_cached_root(&handle);
984 assert_eq!(status, CacheStatus::NotFound);
985 }
986
987 #[test]
988 fn test_list_cached() {
989 let tmp = TempDir::new().unwrap();
990 let cache = PackageCache::new(tmp.path()).unwrap();
991
992 let payload = tmp.path().join("payload");
993 fs::create_dir_all(&payload).unwrap();
994 fs::write(payload.join("file.txt"), b"hello").unwrap();
995
996 let handle = make_handle("mypkg", Some("1.0.0"));
997 cache.add_variant(&handle, &payload, false).unwrap();
998
999 let cached = cache.list_cached();
1000 assert!(!cached.is_empty());
1001 assert_eq!(cached[0].0.name, "mypkg");
1002 }
1003
1004 #[test]
1005 fn test_cache_status_description() {
1006 assert_eq!(CacheStatus::Found.description(), "was found");
1007 assert_eq!(CacheStatus::NotFound.description(), "was not found");
1008 }
1009
1010 #[test]
1011 fn test_increment_string() {
1012 assert_eq!(increment_string("a"), "b");
1013 assert_eq!(increment_string("z"), "aa");
1014 assert_eq!(increment_string("az"), "ba");
1015 }
1016
1017 #[test]
1018 fn test_clean_stats_default() {
1019 let stats = CleanStats::default();
1020 assert_eq!(stats.entries_deleted, 0);
1021 assert_eq!(stats.deleted_bytes, 0);
1022 }
1023}