This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / src / fsio.rs
5.8 kB 178 lines
1use std::path::{Path, PathBuf}; 2use std::time::{Duration, SystemTime}; 3 4use knot_types::{Oid, UnixSeconds}; 5 6use crate::MaintError; 7 8pub(crate) const MIDX_SIDECAR_PREFIX: &str = "multi-pack-index-"; 9 10#[derive(Debug, Clone, PartialEq, Eq, Hash)] 11// Every file in a pack-set has only the one stem, 12// so maintenance finds siblings by just swapping the file extension. 13// Checking the stem once here is more efficient than 14// checking it at each place that makes a sibling path. 15pub struct PackStem(String); 16 17impl PackStem { 18 pub fn of(path: &Path) -> Option<Self> { 19 path.file_stem() 20 .and_then(|stem| stem.to_str()) 21 .filter(|stem| stem.starts_with("pack-") || stem.starts_with(MIDX_SIDECAR_PREFIX)) 22 .map(|stem| Self(stem.to_string())) 23 } 24 25 pub(crate) fn midx_sidecar(checksum_hex: &str) -> Self { 26 Self(format!("{MIDX_SIDECAR_PREFIX}{checksum_hex}")) 27 } 28 29 pub fn file(&self, pack_dir: &Path, extension: &str) -> PathBuf { 30 pack_dir.join(format!("{}.{extension}", self.0)) 31 } 32} 33 34pub fn io_error(path: &Path, error: std::io::Error) -> MaintError { 35 MaintError::Io { 36 path: path.to_path_buf(), 37 message: error.to_string(), 38 } 39} 40 41pub fn loose_objects(objects_dir: &Path) -> Vec<(Oid, PathBuf)> { 42 let Ok(shards) = std::fs::read_dir(objects_dir) else { 43 return Vec::new(); 44 }; 45 shards 46 .filter_map(Result::ok) 47 .filter(|shard| is_shard_name(&shard.file_name())) 48 .flat_map(|shard| loose_in_shard(&shard.path(), &shard.file_name())) 49 .collect() 50} 51 52fn is_shard_name(name: &std::ffi::OsString) -> bool { 53 name.to_str() 54 .is_some_and(|text| text.len() == 2 && text.bytes().all(|byte| byte.is_ascii_hexdigit())) 55} 56 57fn loose_in_shard(shard_path: &Path, shard_name: &std::ffi::OsString) -> Vec<(Oid, PathBuf)> { 58 let Some(prefix) = shard_name.to_str() else { 59 return Vec::new(); 60 }; 61 let Ok(entries) = std::fs::read_dir(shard_path) else { 62 return Vec::new(); 63 }; 64 entries 65 .filter_map(Result::ok) 66 .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_file())) 67 .filter_map(|entry| { 68 let name = entry.file_name(); 69 let rest = name.to_str()?; 70 let oid = Oid::from_hex(&format!("{prefix}{rest}")).ok()?; 71 Some((oid, entry.path())) 72 }) 73 .collect() 74} 75 76pub fn has_loose_refs(git_dir: &Path) -> bool { 77 walkdir::WalkDir::new(git_dir.join("refs")) 78 .into_iter() 79 .filter_map(Result::ok) 80 .any(|entry| entry.file_type().is_file()) 81} 82 83pub fn pack_idx_paths(objects_dir: &Path) -> Vec<PathBuf> { 84 let pack_dir = objects_dir.join("pack"); 85 let Ok(entries) = std::fs::read_dir(&pack_dir) else { 86 return Vec::new(); 87 }; 88 entries 89 .filter_map(Result::ok) 90 .map(|entry| entry.path()) 91 .filter(|path| path.extension().is_some_and(|ext| ext == "idx")) 92 .collect() 93} 94 95pub fn pack_oids(idx: &Path, kind: gix::hash::Kind) -> Option<Vec<Oid>> { 96 let index = gix_pack::index::File::at(idx, kind).ok()?; 97 Some(index.iter().map(|entry| Oid::from(entry.oid)).collect()) 98} 99 100pub fn pack_file(idx: &Path, pack_dir: &Path) -> Option<PathBuf> { 101 let stem = idx.file_stem().and_then(|stem| stem.to_str())?; 102 Some(pack_dir.join(format!("{stem}.pack"))) 103} 104 105pub fn remove_pack_files(idx: &Path, pack_dir: &Path) -> bool { 106 let Some(stem) = idx.file_stem().and_then(|stem| stem.to_str()) else { 107 return false; 108 }; 109 let idx_removed = std::fs::remove_file(idx).is_ok(); 110 let pack_removed = std::fs::remove_file(pack_dir.join(format!("{stem}.pack"))).is_ok(); 111 let _ = std::fs::remove_file(pack_dir.join(format!("{stem}.rev"))); 112 let _ = std::fs::remove_file(pack_dir.join(format!("{stem}.bitmap"))); 113 let _ = std::fs::remove_file(pack_dir.join(format!("{stem}.mtimes"))); 114 idx_removed || pack_removed 115} 116 117pub fn pack_mtime(idx: &Path, pack_dir: &Path) -> Option<UnixSeconds> { 118 let pack = pack_file(idx, pack_dir)?; 119 let modified = pack.metadata().ok()?.modified().ok()?; 120 let secs = modified 121 .duration_since(SystemTime::UNIX_EPOCH) 122 .ok()? 123 .as_secs(); 124 Some(UnixSeconds::new(secs as i64)) 125} 126 127pub fn older_than(path: &Path, grace: Duration) -> bool { 128 path.metadata() 129 .and_then(|meta| meta.modified()) 130 .map(|modified| { 131 SystemTime::now() 132 .duration_since(modified) 133 .unwrap_or(Duration::ZERO) 134 >= grace 135 }) 136 .unwrap_or(false) 137} 138 139#[cfg(test)] 140mod tests { 141 use super::*; 142 143 fn touch(path: &Path) { 144 std::fs::write(path, b"x").unwrap(); 145 } 146 147 #[test] 148 fn remove_pack_files_clears_idx_and_pack_together() { 149 let dir = tempfile::tempdir().unwrap(); 150 let pack_dir = dir.path(); 151 let idx = pack_dir.join("pack-scallop.idx"); 152 touch(&idx); 153 touch(&pack_dir.join("pack-scallop.pack")); 154 touch(&pack_dir.join("pack-scallop.mtimes")); 155 156 assert!(remove_pack_files(&idx, pack_dir)); 157 assert!(!idx.exists()); 158 assert!(!pack_dir.join("pack-scallop.pack").exists()); 159 assert!(!pack_dir.join("pack-scallop.mtimes").exists()); 160 } 161 162 #[test] 163 fn remove_pack_files_reclaims_an_orphan_idx_with_no_pack() { 164 let dir = tempfile::tempdir().unwrap(); 165 let pack_dir = dir.path(); 166 let idx = pack_dir.join("pack-whelk.idx"); 167 touch(&idx); 168 169 assert!( 170 remove_pack_files(&idx, pack_dir), 171 "a lone idx left by a crash mid-deletion is still reclaimed" 172 ); 173 assert!( 174 !idx.exists(), 175 "the orphan idx no longer advertises phantom objects" 176 ); 177 } 178}