|
1 | 1 | //! Local POSIX file system volume implementation. |
2 | 2 |
|
3 | 3 | use super::{ |
4 | | - CopyScanResult, ScanConflict, SourceItemInfo, SpaceInfo, Volume, VolumeError, VolumeScanner, VolumeWatcher, |
| 4 | + CopyScanResult, ScanConflict, SourceItemInfo, SpaceInfo, Volume, VolumeError, VolumeReadStream, VolumeScanner, |
| 5 | + VolumeWatcher, |
5 | 6 | }; |
6 | 7 | use crate::file_system::listing::{FileEntry, get_single_entry, list_directory_core}; |
7 | 8 | use crate::indexing::scanner::{self, ScanConfig, ScanError, ScanHandle, ScanSummary}; |
@@ -295,33 +296,102 @@ impl Volume for LocalPosixVolume { |
295 | 296 | }) |
296 | 297 | } |
297 | 298 |
|
298 | | - fn export_to_local<'a>( |
| 299 | + fn supports_streaming(&self) -> bool { |
| 300 | + true |
| 301 | + } |
| 302 | + |
| 303 | + fn open_read_stream<'a>( |
299 | 304 | &'a self, |
300 | | - source: &'a Path, |
301 | | - local_dest: &'a Path, |
302 | | - _on_progress: &'a (dyn Fn(u64, u64) -> std::ops::ControlFlow<()> + Sync), |
303 | | - ) -> Pin<Box<dyn Future<Output = Result<u64, VolumeError>> + Send + 'a>> { |
304 | | - let src_abs = self.resolve(source); |
305 | | - let local_dest = local_dest.to_path_buf(); |
| 305 | + path: &'a Path, |
| 306 | + ) -> Pin<Box<dyn Future<Output = Result<Box<dyn VolumeReadStream>, VolumeError>> + Send + 'a>> { |
| 307 | + let abs_path = self.resolve(path); |
306 | 308 | Box::pin(async move { |
307 | | - spawn_blocking(move || copy_recursive(&src_abs, &local_dest)) |
308 | | - .await |
309 | | - .unwrap() |
| 309 | + spawn_blocking(move || { |
| 310 | + let metadata = std::fs::metadata(&abs_path)?; |
| 311 | + if metadata.is_dir() { |
| 312 | + return Err(VolumeError::IoError { |
| 313 | + message: "Cannot stream a directory".into(), |
| 314 | + raw_os_error: None, |
| 315 | + }); |
| 316 | + } |
| 317 | + let total_size = metadata.len(); |
| 318 | + let file = std::fs::File::open(&abs_path)?; |
| 319 | + Ok(Box::new(LocalPosixReadStream { |
| 320 | + file: Some(file), |
| 321 | + total_size, |
| 322 | + bytes_read: 0, |
| 323 | + }) as Box<dyn VolumeReadStream>) |
| 324 | + }) |
| 325 | + .await |
| 326 | + .unwrap() |
310 | 327 | }) |
311 | 328 | } |
312 | 329 |
|
313 | | - fn import_from_local<'a>( |
| 330 | + fn write_from_stream<'a>( |
314 | 331 | &'a self, |
315 | | - local_source: &'a Path, |
316 | 332 | dest: &'a Path, |
317 | | - _on_progress: &'a (dyn Fn(u64, u64) -> std::ops::ControlFlow<()> + Sync), |
| 333 | + size: u64, |
| 334 | + mut stream: Box<dyn VolumeReadStream>, |
| 335 | + on_progress: &'a (dyn Fn(u64, u64) -> std::ops::ControlFlow<()> + Sync), |
318 | 336 | ) -> Pin<Box<dyn Future<Output = Result<u64, VolumeError>> + Send + 'a>> { |
319 | | - let local_source = local_source.to_path_buf(); |
320 | 337 | let dest_abs = self.resolve(dest); |
321 | 338 | Box::pin(async move { |
322 | | - spawn_blocking(move || copy_recursive(&local_source, &dest_abs)) |
| 339 | + // Ensure parent directory exists |
| 340 | + if let Some(parent) = dest_abs.parent() { |
| 341 | + let parent = parent.to_path_buf(); |
| 342 | + spawn_blocking(move || std::fs::create_dir_all(&parent)) |
| 343 | + .await |
| 344 | + .unwrap() |
| 345 | + .map_err(VolumeError::from)?; |
| 346 | + } |
| 347 | + |
| 348 | + // Open destination file on the blocking pool. |
| 349 | + let dest_for_open = dest_abs.clone(); |
| 350 | + let mut file = spawn_blocking(move || std::fs::File::create(&dest_for_open)) |
323 | 351 | .await |
324 | 352 | .unwrap() |
| 353 | + .map_err(VolumeError::from)?; |
| 354 | + |
| 355 | + let mut bytes_written = 0u64; |
| 356 | + while let Some(chunk_result) = stream.next_chunk().await { |
| 357 | + let chunk = chunk_result?; |
| 358 | + if chunk.is_empty() { |
| 359 | + continue; |
| 360 | + } |
| 361 | + let chunk_len = chunk.len() as u64; |
| 362 | + |
| 363 | + // Write the chunk on the blocking pool. |
| 364 | + let (file_ret, write_res) = spawn_blocking(move || { |
| 365 | + use std::io::Write; |
| 366 | + let res = file.write_all(&chunk); |
| 367 | + (file, res) |
| 368 | + }) |
| 369 | + .await |
| 370 | + .unwrap(); |
| 371 | + file = file_ret; |
| 372 | + write_res.map_err(VolumeError::from)?; |
| 373 | + |
| 374 | + bytes_written += chunk_len; |
| 375 | + |
| 376 | + if on_progress(bytes_written, size) == std::ops::ControlFlow::Break(()) { |
| 377 | + // Drop the file handle and try to clean up the partial file. |
| 378 | + drop(file); |
| 379 | + let partial = dest_abs.clone(); |
| 380 | + let _ = spawn_blocking(move || std::fs::remove_file(&partial)).await; |
| 381 | + return Err(VolumeError::Cancelled("Operation cancelled by user".to_string())); |
| 382 | + } |
| 383 | + } |
| 384 | + |
| 385 | + // Flush and close on the blocking pool. |
| 386 | + let flush_res = spawn_blocking(move || { |
| 387 | + use std::io::Write; |
| 388 | + file.flush() |
| 389 | + }) |
| 390 | + .await |
| 391 | + .unwrap(); |
| 392 | + flush_res.map_err(VolumeError::from)?; |
| 393 | + |
| 394 | + Ok(bytes_written) |
325 | 395 | }) |
326 | 396 | } |
327 | 397 |
|
@@ -416,30 +486,65 @@ impl VolumeWatcher for LocalPosixWatcher { |
416 | 486 | } |
417 | 487 | } |
418 | 488 |
|
419 | | -/// Recursively copies a file or directory from source to destination. |
420 | | -/// Returns total bytes copied. |
421 | | -fn copy_recursive(source: &Path, dest: &Path) -> Result<u64, VolumeError> { |
422 | | - let meta = std::fs::metadata(source)?; |
423 | | - let mut total_bytes = 0; |
424 | | - |
425 | | - if meta.is_file() { |
426 | | - // Copy single file |
427 | | - std::fs::copy(source, dest)?; |
428 | | - total_bytes = meta.len(); |
429 | | - } else if meta.is_dir() { |
430 | | - // Create destination directory |
431 | | - std::fs::create_dir_all(dest)?; |
432 | | - |
433 | | - // Copy all contents |
434 | | - for entry in std::fs::read_dir(source)? { |
435 | | - let entry = entry?; |
436 | | - let src_path = entry.path(); |
437 | | - let dest_path = dest.join(entry.file_name()); |
438 | | - total_bytes += copy_recursive(&src_path, &dest_path)?; |
439 | | - } |
| 489 | +/// Streaming reader for `LocalPosixVolume` files. |
| 490 | +/// |
| 491 | +/// Reads the file in 1 MiB chunks on the blocking thread pool via |
| 492 | +/// `tokio::task::spawn_blocking`. Each `next_chunk` call hands the file handle |
| 493 | +/// to the blocking pool, reads one chunk, and returns ownership along with the |
| 494 | +/// data. |
| 495 | +struct LocalPosixReadStream { |
| 496 | + file: Option<std::fs::File>, |
| 497 | + total_size: u64, |
| 498 | + bytes_read: u64, |
| 499 | +} |
| 500 | + |
| 501 | +/// 1 MiB chunks — matches `chunked_copy.rs`'s constant. |
| 502 | +const LOCAL_STREAM_CHUNK_SIZE: usize = 1024 * 1024; |
| 503 | + |
| 504 | +impl VolumeReadStream for LocalPosixReadStream { |
| 505 | + fn next_chunk(&mut self) -> Pin<Box<dyn Future<Output = Option<Result<Vec<u8>, VolumeError>>> + Send + '_>> { |
| 506 | + Box::pin(async move { |
| 507 | + let mut file = self.file.take()?; |
| 508 | + |
| 509 | + let (file_ret, result) = spawn_blocking(move || { |
| 510 | + use std::io::Read; |
| 511 | + let mut buf = vec![0u8; LOCAL_STREAM_CHUNK_SIZE]; |
| 512 | + let n = match file.read(&mut buf) { |
| 513 | + Ok(n) => n, |
| 514 | + Err(e) => return (file, Err(VolumeError::from(e))), |
| 515 | + }; |
| 516 | + buf.truncate(n); |
| 517 | + (file, Ok(buf)) |
| 518 | + }) |
| 519 | + .await |
| 520 | + .unwrap(); |
| 521 | + |
| 522 | + match result { |
| 523 | + Ok(buf) if buf.is_empty() => { |
| 524 | + // EOF — drop the file handle. |
| 525 | + drop(file_ret); |
| 526 | + None |
| 527 | + } |
| 528 | + Ok(buf) => { |
| 529 | + self.bytes_read += buf.len() as u64; |
| 530 | + self.file = Some(file_ret); |
| 531 | + Some(Ok(buf)) |
| 532 | + } |
| 533 | + Err(e) => { |
| 534 | + drop(file_ret); |
| 535 | + Some(Err(e)) |
| 536 | + } |
| 537 | + } |
| 538 | + }) |
440 | 539 | } |
441 | 540 |
|
442 | | - Ok(total_bytes) |
| 541 | + fn total_size(&self) -> u64 { |
| 542 | + self.total_size |
| 543 | + } |
| 544 | + |
| 545 | + fn bytes_read(&self) -> u64 { |
| 546 | + self.bytes_read |
| 547 | + } |
443 | 548 | } |
444 | 549 |
|
445 | 550 | /// Gets space information for a path. |
|
0 commit comments