objects/store/fs/
fs_io.rs1#![deny(clippy::cast_possible_truncation)]
3
4use std::{
7 collections::BTreeSet,
8 fs::File,
9 io::Read,
10 path::{Path, PathBuf},
11 sync::Mutex,
12};
13
14use bytes::Bytes;
15
16use crate::{
17 error::HeddleError,
18 fs_atomic::{
19 create_dir_all_durable, enrich_fs_error, enrich_rename_error, sync_directory, temp_path,
20 },
21 store::Result,
22};
23
24const MMAP_THRESHOLD_BYTES: u64 = 256 * 1024;
25
26pub(super) enum FileBytes {
27 Vec(Vec<u8>),
28 Mmap(memmap2::Mmap),
29}
30
31impl FileBytes {
32 pub(super) fn as_slice(&self) -> &[u8] {
33 match self {
34 FileBytes::Vec(data) => data,
35 FileBytes::Mmap(data) => data,
36 }
37 }
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub(super) enum AtomicWriteMode {
42 Durable,
43 BatchDirectorySync,
44 NoSync,
54}
55
56pub(super) fn write_atomic(
57 path: &Path,
58 data: &[u8],
59 mode: AtomicWriteMode,
60 pending_directory_syncs: Option<&Mutex<BTreeSet<PathBuf>>>,
61) -> Result<()> {
62 let parent = path
63 .parent()
64 .ok_or_else(|| std::io::Error::other("invalid atomic write path"))?;
65 create_dir_all_durable(parent)
66 .map_err(|e| HeddleError::Io(enrich_fs_error(parent, "creating", e)))?;
67
68 let temp_path = temp_path(path);
69 enum Op {
77 Write,
78 Rename,
79 SyncDir,
80 }
81 let mut failing_op = Op::Write;
82 let write_result: std::io::Result<()> = (|| {
83 let mut opts = std::fs::OpenOptions::new();
92 opts.write(true).create_new(true);
93 #[cfg(unix)]
94 {
95 use std::os::unix::fs::OpenOptionsExt;
96 opts.mode(0o644);
97 }
98 let mut file = opts.open(&temp_path)?;
99 use std::io::Write as _;
100 file.write_all(data)?;
101 match mode {
102 AtomicWriteMode::Durable => file.sync_all()?,
107 AtomicWriteMode::BatchDirectorySync => file.sync_data()?,
119 AtomicWriteMode::NoSync => {}
123 }
124 failing_op = Op::Rename;
125 std::fs::rename(&temp_path, path)?;
126 failing_op = Op::SyncDir;
127 match mode {
128 AtomicWriteMode::Durable => sync_directory(parent)?,
129 AtomicWriteMode::BatchDirectorySync => {
130 if let Some(pending) = pending_directory_syncs {
131 let mut dirs = pending.lock().map_err(|_| {
132 std::io::Error::other("failed to acquire pending directory sync lock")
133 })?;
134 dirs.insert(parent.to_path_buf());
135 }
136 }
137 AtomicWriteMode::NoSync => {}
138 }
139 Ok(())
140 })();
141
142 if let Err(err) = write_result {
143 let _ = std::fs::remove_file(&temp_path);
144 let wrapped = match failing_op {
145 Op::Write => enrich_fs_error(path, "writing", err),
146 Op::Rename => enrich_rename_error(&temp_path, path, err),
147 Op::SyncDir => enrich_fs_error(parent, "syncing", err),
148 };
149 return Err(HeddleError::Io(wrapped));
150 }
151
152 Ok(())
153}
154
155pub(super) fn read_file_header(path: &Path, header_len: usize) -> Result<Option<(Vec<u8>, u64)>> {
164 let mut file = match File::open(path) {
165 Ok(file) => file,
166 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
167 Err(e) => return Err(e.into()),
168 };
169
170 let metadata = file.metadata()?;
171 let len = metadata.len();
172 let to_read = if len > header_len as u64 {
173 header_len
174 } else {
175 checked_file_len_to_usize(len)?
176 };
177 let mut header = vec![0u8; to_read];
178 if to_read > 0 {
179 use std::io::Read as _;
180 file.read_exact(&mut header)?;
181 }
182 Ok(Some((header, len)))
183}
184
185pub fn read_file_bytes_for_pack(path: &Path) -> Result<Bytes> {
192 let file = File::open(path)?;
193 let len = file.metadata()?.len();
194 if len == 0 {
195 return Ok(Bytes::new());
196 }
197 if len >= MMAP_THRESHOLD_BYTES {
198 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
199 if mmap.len() != checked_file_len_to_usize(len)? {
200 return Err(HeddleError::InvalidObject(
201 "pack file size changed during memory mapping".to_string(),
202 ));
203 }
204 return Ok(Bytes::from_owner(mmap));
205 }
206 let mut data = Vec::with_capacity(checked_file_len_to_usize(len)?);
207 let mut reader = file;
208 reader.read_to_end(&mut data)?;
209 Ok(Bytes::from(data))
210}
211
212pub(super) fn read_file_bytes(path: &Path) -> Result<Option<FileBytes>> {
213 let file = match File::open(path) {
214 Ok(file) => file,
215 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
216 Err(e) => return Err(e.into()),
217 };
218
219 let metadata = file.metadata()?;
220 let len = metadata.len();
221 if len == 0 {
222 return Ok(Some(FileBytes::Vec(vec![])));
223 }
224 if len >= MMAP_THRESHOLD_BYTES {
225 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
226 if mmap.len() != checked_file_len_to_usize(len)? {
227 return Err(crate::store::HeddleError::InvalidObject(
228 "file size changed during memory mapping".to_string(),
229 ));
230 }
231 return Ok(Some(FileBytes::Mmap(mmap)));
232 }
233
234 let mut data = Vec::with_capacity(checked_file_len_to_usize(len)?);
235 let mut reader = file;
236 reader.read_to_end(&mut data)?;
237 Ok(Some(FileBytes::Vec(data)))
238}
239
240fn checked_file_len_to_usize(len: u64) -> Result<usize> {
241 usize::try_from(len).map_err(|_| {
242 HeddleError::InvalidObject(format!("file length {len} exceeds platform limits"))
243 })
244}
245
246pub(super) fn list_hashes_from_dir(
248 dir: &std::path::Path,
249) -> Result<Vec<crate::object::ContentHash>> {
250 use std::fs;
251
252 use tracing::debug;
253
254 if !dir.exists() {
255 return Ok(Vec::new());
256 }
257
258 let mut hashes = Vec::new();
259 for entry in fs::read_dir(dir)? {
260 let entry = entry?;
261 let path = entry.path();
262 if path.is_dir() {
263 let prefix = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
264 if prefix.len() == 2 {
265 for sub_entry in fs::read_dir(&path)? {
266 let sub_entry = sub_entry?;
267 let sub_path = sub_entry.path();
268 if let Some(name) = sub_path.file_name().and_then(|n| n.to_str()) {
269 let full_hash = format!("{}{}", prefix, name);
270 if let Ok(hash) = crate::object::ContentHash::from_hex(&full_hash) {
271 hashes.push(hash);
272 }
273 }
274 }
275 }
276 }
277 }
278 debug!(count = hashes.len(), "Listed hashes");
279 Ok(hashes)
280}