1use std::collections::HashSet;
7use std::fs::File;
8use std::io::{ErrorKind, Read, SeekFrom};
9use std::ops::Range;
10use std::sync::Arc;
11
12#[cfg(unix)]
14use std::os::unix::fs::FileExt;
15#[cfg(windows)]
16use std::os::windows::fs::FileExt;
17
18use async_trait::async_trait;
19use bytes::{Bytes, BytesMut};
20use chrono::{DateTime, Utc};
21use futures::future::BoxFuture;
22use lance_core::deepsize::DeepSizeOf;
23use lance_core::{Error, Result};
24use object_store::path::Path;
25use tokio::io::AsyncSeekExt;
26use tokio::sync::OnceCell;
27use tracing::instrument;
28
29use crate::object_reader::stream_local_range;
30use crate::object_store::DEFAULT_LOCAL_IO_PARALLELISM;
31use crate::object_writer::WriteResult;
32use crate::traits::{ByteStream, Reader, Writer};
33use crate::utils::tracking_store::IOTracker;
34
35pub fn to_local_path(path: &Path) -> String {
37 if cfg!(windows) {
38 path.to_string()
39 } else {
40 format!("/{path}")
41 }
42}
43
44pub fn remove_dir_all(path: &Path) -> Result<()> {
46 std::fs::remove_dir_all(to_local_path(path)).map_err(|err| match err.kind() {
47 ErrorKind::NotFound => Error::not_found(path.to_string()),
48 _ => Error::from(err),
49 })?;
50 Ok(())
51}
52
53pub(crate) fn remove_empty_dirs(
55 root: &Path,
56 retained_dirs: &HashSet<Path>,
57 verified_dirs: &HashSet<Path>,
58 unmodified_since: Option<DateTime<Utc>>,
59) -> Result<()> {
60 let root_path = std::path::PathBuf::from(to_local_path(root));
61 let root_metadata = match std::fs::symlink_metadata(&root_path) {
62 Ok(metadata) => metadata,
63 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
64 Err(err) => return Err(Error::from(err)),
65 };
66 if !root_metadata.file_type().is_dir() {
67 return Ok(());
68 }
69 let canonical_root = std::fs::canonicalize(&root_path)?;
70
71 let mut pending_dirs = vec![(root_path, root.clone(), None::<Path>)];
72 let mut discovered_dirs = Vec::new();
73 let mut file_bearing_roots = HashSet::new();
74 let mut first_error = None;
75 while let Some((local_dir, object_store_dir, index_root)) = pending_dirs.pop() {
76 let entries = match std::fs::read_dir(&local_dir) {
77 Ok(entries) => entries,
78 Err(err) if err.kind() == ErrorKind::NotFound => continue,
79 Err(err) => return Err(Error::from(err)),
80 };
81 for entry in entries {
82 let entry = entry?;
83 if !entry.file_type()?.is_dir() {
86 if let Some(index_root) = &index_root {
87 file_bearing_roots.insert(index_root.clone());
88 }
89 continue;
90 }
91 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
92 continue;
93 };
94 let child = object_store_dir.clone().join(name);
95 if retained_dirs.contains(&child) {
96 continue;
97 }
98 let local_child = entry.path();
99 let index_root = index_root.clone().unwrap_or_else(|| child.clone());
100 let is_old_enough = match std::fs::symlink_metadata(&local_child) {
103 Ok(metadata) => unmodified_since.is_none_or(|threshold| {
104 metadata
105 .modified()
106 .ok()
107 .map(DateTime::<Utc>::from)
108 .is_some_and(|modified| modified < threshold)
109 }),
110 Err(err) if err.kind() == ErrorKind::NotFound => continue,
111 Err(err) => {
112 first_error.get_or_insert_with(|| Error::from(err));
113 false
114 }
115 };
116 discovered_dirs.push((
117 local_child.clone(),
118 child.clone(),
119 index_root.clone(),
120 is_old_enough,
121 ));
122 pending_dirs.push((local_child, child, Some(index_root)));
123 }
124 }
125
126 discovered_dirs.sort_unstable_by_key(|(_, path, _, _)| std::cmp::Reverse(path.parts_count()));
127 for (local_dir, object_store_dir, index_root, is_old_enough) in discovered_dirs {
128 if file_bearing_roots.contains(&index_root) {
129 continue;
130 }
131 match std::fs::symlink_metadata(&local_dir) {
132 Ok(metadata) if metadata.file_type().is_dir() => {}
133 Ok(_) => continue,
134 Err(err) if err.kind() == ErrorKind::NotFound => continue,
135 Err(err) => {
136 first_error.get_or_insert_with(|| Error::from(err));
137 continue;
138 }
139 }
140 let is_verified = verified_dirs.contains(&object_store_dir);
141 if !is_verified && !is_old_enough {
142 continue;
143 }
144
145 let canonical_dir = match std::fs::canonicalize(&local_dir) {
149 Ok(path) if path != canonical_root && path.starts_with(&canonical_root) => path,
150 Ok(_) => continue,
151 Err(err) if err.kind() == ErrorKind::NotFound => continue,
152 Err(err) => {
153 first_error.get_or_insert_with(|| Error::from(err));
154 continue;
155 }
156 };
157 if let Err(err) = std::fs::remove_dir(canonical_dir)
158 && !matches!(
159 err.kind(),
160 ErrorKind::NotFound | ErrorKind::DirectoryNotEmpty
161 )
162 {
163 first_error.get_or_insert_with(|| Error::from(err));
164 }
165 }
166
167 if let Some(error) = first_error {
168 Err(error)
169 } else {
170 Ok(())
171 }
172}
173
174pub fn copy_file(from: &Path, to: &Path) -> Result<()> {
178 let from_path = to_local_path(from);
179 let to_path = to_local_path(to);
180
181 if let Some(parent) = std::path::Path::new(&to_path).parent() {
183 std::fs::create_dir_all(parent).map_err(Error::from)?;
184 }
185
186 std::fs::copy(&from_path, &to_path).map_err(|err| match err.kind() {
187 ErrorKind::NotFound => Error::not_found(from.to_string()),
188 _ => Error::from(err),
189 })?;
190 Ok(())
191}
192
193pub(crate) async fn join_local_io<T>(
200 handle: tokio::task::JoinHandle<std::io::Result<T>>,
201) -> object_store::Result<T> {
202 match handle.await {
203 Ok(result) => result.map_err(|err| object_store::Error::Generic {
204 store: "LocalFileSystem",
205 source: err.into(),
206 }),
207 Err(err) => Err(err.into()),
208 }
209}
210
211#[derive(Debug)]
213pub struct LocalObjectReader {
214 file: Arc<File>,
216
217 path: Path,
219
220 size: OnceCell<usize>,
223
224 block_size: usize,
226
227 io_tracker: Arc<IOTracker>,
229}
230
231impl DeepSizeOf for LocalObjectReader {
232 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
233 self.path.as_ref().deep_size_of_children(context)
235 }
236}
237
238impl LocalObjectReader {
239 pub async fn open_local_path(
240 path: impl AsRef<std::path::Path>,
241 block_size: usize,
242 known_size: Option<usize>,
243 ) -> Result<Box<dyn Reader>> {
244 let path = path.as_ref().to_owned();
245 let object_store_path = Path::from_filesystem_path(&path)?;
246 Self::open(&object_store_path, block_size, known_size).await
247 }
248
249 #[instrument(level = "debug")]
253 pub async fn open(
254 path: &Path,
255 block_size: usize,
256 known_size: Option<usize>,
257 ) -> Result<Box<dyn Reader>> {
258 Self::open_with_tracker(path, block_size, known_size, Default::default()).await
259 }
260
261 #[instrument(level = "debug")]
263 pub(crate) async fn open_with_tracker(
264 path: &Path,
265 block_size: usize,
266 known_size: Option<usize>,
267 io_tracker: Arc<IOTracker>,
268 ) -> Result<Box<dyn Reader>> {
269 let path = path.clone();
270 let local_path = to_local_path(&path);
271 tokio::task::spawn_blocking(move || {
272 let file = File::open(&local_path).map_err(|e| match e.kind() {
273 ErrorKind::NotFound => Error::not_found(path.to_string()),
274 _ => e.into(),
275 })?;
276 let size = OnceCell::new_with(known_size);
277 Ok(Box::new(Self {
278 file: Arc::new(file),
279 block_size,
280 size,
281 path,
282 io_tracker,
283 }) as Box<dyn Reader>)
284 })
285 .await?
286 }
287}
288
289impl Reader for LocalObjectReader {
290 fn path(&self) -> &Path {
291 &self.path
292 }
293
294 fn block_size(&self) -> usize {
295 self.block_size
296 }
297
298 fn io_parallelism(&self) -> usize {
299 DEFAULT_LOCAL_IO_PARALLELISM
300 }
301
302 fn size(&self) -> BoxFuture<'_, object_store::Result<usize>> {
304 Box::pin(async move {
305 let file = self.file.clone();
306 self.size
307 .get_or_try_init(|| async move {
308 let metrics = self.io_tracker.begin_io("head");
311 let result =
312 join_local_io(tokio::task::spawn_blocking(move || file.metadata())).await;
313 metrics.record(&result, 0);
314 Ok(result?.len() as usize)
315 })
316 .await
317 .cloned()
318 })
319 }
320
321 #[instrument(level = "debug", skip(self))]
323 fn get_range(&self, range: Range<usize>) -> BoxFuture<'static, object_store::Result<Bytes>> {
324 let file = self.file.clone();
325 let io_tracker = self.io_tracker.clone();
326 let path = self.path.clone();
327 let num_bytes = range.len() as u64;
328 let range_u64 = (range.start as u64)..(range.end as u64);
329
330 Box::pin(async move {
331 let metrics = io_tracker.begin_io("get");
332 let result = join_local_io(tokio::task::spawn_blocking(move || {
333 let mut buf = BytesMut::with_capacity(range.len());
334 unsafe { buf.set_len(range.len()) };
337 #[cfg(unix)]
338 file.read_exact_at(buf.as_mut(), range.start as u64)?;
339 #[cfg(windows)]
340 read_exact_at(file, buf.as_mut(), range.start as u64)?;
341
342 Ok(buf.freeze())
343 }))
344 .await;
345
346 metrics.record(&result, num_bytes);
347 if result.is_ok() {
348 io_tracker.record_read("get_range", path, num_bytes, Some(range_u64));
349 }
350
351 result
352 })
353 }
354
355 #[instrument(level = "debug", skip(self))]
357 fn get_all(&self) -> BoxFuture<'_, object_store::Result<Bytes>> {
358 Box::pin(async move {
359 let mut file = self.file.clone();
360 let io_tracker = self.io_tracker.clone();
361 let path = self.path.clone();
362
363 let metrics = io_tracker.begin_io("get");
364 let result = join_local_io(tokio::task::spawn_blocking(move || {
365 let mut buf = Vec::new();
366 file.read_to_end(buf.as_mut())?;
367 Ok(Bytes::from(buf))
368 }))
369 .await;
370
371 let num_bytes = result.as_ref().map_or(0, |bytes| bytes.len() as u64);
372 metrics.record(&result, num_bytes);
373 if let Ok(bytes) = &result {
374 io_tracker.record_read("get_all", path, bytes.len() as u64, None);
375 }
376
377 result
378 })
379 }
380
381 fn get_stream(&self) -> BoxFuture<'_, object_store::Result<ByteStream>> {
382 Box::pin(async move {
383 let size = self.size().await?;
384 Ok(stream_local_range(
385 self.file.clone(),
386 self.path.clone(),
387 self.io_tracker.clone(),
388 0..size,
389 self.block_size.max(8 * 1024),
390 ))
391 })
392 }
393
394 fn get_range_stream(
395 &self,
396 range: Range<usize>,
397 ) -> BoxFuture<'_, object_store::Result<ByteStream>> {
398 let file = self.file.clone();
399 let path = self.path.clone();
400 let io_tracker = self.io_tracker.clone();
401 let chunk_size = self.block_size.max(8 * 1024);
402 Box::pin(async move {
403 Ok(stream_local_range(
404 file, path, io_tracker, range, chunk_size,
405 ))
406 })
407 }
408}
409
410#[cfg(windows)]
411pub(crate) fn read_exact_at(
412 file: Arc<File>,
413 mut buf: &mut [u8],
414 mut offset: u64,
415) -> std::io::Result<()> {
416 let expected_len = buf.len();
417 while !buf.is_empty() {
418 match file.seek_read(buf, offset) {
419 Ok(0) => break,
420 Ok(n) => {
421 let tmp = buf;
422 buf = &mut tmp[n..];
423 offset += n as u64;
424 }
425 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
426 Err(e) => return Err(e),
427 }
428 }
429 if !buf.is_empty() {
430 Err(std::io::Error::new(
431 std::io::ErrorKind::UnexpectedEof,
432 format!(
433 "failed to fill whole buffer. Expected {} bytes, got {}",
434 expected_len, offset
435 ),
436 ))
437 } else {
438 Ok(())
439 }
440}
441
442#[async_trait]
443impl Writer for tokio::fs::File {
444 async fn tell(&mut self) -> Result<usize> {
445 Ok(self.seek(SeekFrom::Current(0)).await? as usize)
446 }
447
448 async fn shutdown(&mut self) -> Result<WriteResult> {
449 let size = self.seek(SeekFrom::Current(0)).await? as usize;
450 tokio::io::AsyncWriteExt::shutdown(self).await?;
451 Ok(WriteResult { size, e_tag: None })
452 }
453}