This repository has no description
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}