1use std::path::{Path, PathBuf};
4use std::pin::Pin;
5
6use async_trait::async_trait;
7use bytes::Bytes;
8use futures_util::{Stream, StreamExt};
9use tokio::io::AsyncReadExt;
10
11use crate::atomic_file::{FileError, FileSync};
12
13pub const TEMP_BLOB_PREFIX: &str = crate::atomic_file::TEMP_FILE_PREFIX;
17
18pub(crate) fn is_temp_blob_path(path: &Path) -> bool {
20 path.file_name()
21 .and_then(|name| name.to_str())
22 .is_some_and(|name| name.starts_with(TEMP_BLOB_PREFIX))
23}
24
25#[derive(Debug)]
26pub enum StreamWriteError<E> {
27 Source(E),
28 Local(FileError),
29}
30
31impl<E: std::fmt::Display> std::fmt::Display for StreamWriteError<E> {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::Source(error) => error.fmt(f),
35 Self::Local(error) => write!(f, "local destination: {error}"),
36 }
37 }
38}
39
40impl<E: std::fmt::Debug + std::fmt::Display> std::error::Error for StreamWriteError<E> {}
41
42#[derive(Debug)]
43pub enum ByteStreamWriteError<E> {
44 Source(E),
45 SourceCleanup { source: E, cleanup: FileError },
46 Local(FileError),
47}
48
49#[async_trait]
50pub trait PlaintextChunkReader: Send {
51 type Error: Send;
52 async fn next_chunk(&mut self, max: usize) -> Result<Vec<u8>, Self::Error>;
53}
54
55enum AtomicChunkWriteError<E> {
56 Source(E),
57 Local(FileError),
58}
59
60struct AtomicTempFile {
61 path: PathBuf,
62 file: Option<tokio::fs::File>,
63 armed: bool,
64}
65
66pub struct AtomicStagedFile {
69 destination: PathBuf,
70 staged: Option<AtomicTempFile>,
71 file_sync: FileSync,
72}
73
74pub struct PublishedAtomicFile {
77 destination: PathBuf,
78 file_sync: FileSync,
79}
80
81#[derive(Debug, thiserror::Error)]
82pub enum CommitNewFileError {
83 #[error("destination already exists: {0}")]
84 DestinationExists(PathBuf),
85 #[error("commit new file: {0}")]
86 Filesystem(#[from] FileError),
87 #[error("{operation}; rollback failed: {rollback}")]
88 RollbackFailed {
89 operation: Box<CommitNewFileError>,
90 rollback: Box<FileError>,
91 },
92}
93
94impl CommitNewFileError {
95 fn rollback(operation: CommitNewFileError, rollback: FileError) -> Self {
96 Self::RollbackFailed {
97 operation: Box::new(operation),
98 rollback: Box::new(rollback),
99 }
100 }
101}
102
103impl AtomicStagedFile {
104 pub(crate) fn is_staging_path(path: &Path) -> bool {
105 is_temp_blob_path(path)
106 }
107
108 pub async fn create(destination: &Path) -> Result<Self, FileError> {
109 Self::create_with_file_sync(destination, FileSync::Enabled).await
110 }
111
112 pub(crate) async fn create_with_file_sync(
113 destination: &Path,
114 file_sync: FileSync,
115 ) -> Result<Self, FileError> {
116 let parent = destination.parent().ok_or_else(|| FileError::NoParent {
117 path: destination.to_path_buf(),
118 })?;
119 tokio::fs::create_dir_all(parent)
120 .await
121 .map_err(|source| FileError::at("create parent directory", parent, source))?;
122 let staged = AtomicTempFile::create_in(parent)?;
123 Ok(Self {
124 destination: destination.to_path_buf(),
125 staged: Some(staged),
126 file_sync,
127 })
128 }
129
130 pub fn path(&self) -> &Path {
131 &self
132 .staged
133 .as_ref()
134 .expect("atomic stage is unpublished")
135 .path
136 }
137
138 pub fn destination(&self) -> &Path {
139 &self.destination
140 }
141
142 pub async fn stage_peer(&self, destination: &Path) -> Result<Self, FileError> {
144 Self::create_with_file_sync(destination, self.file_sync.clone()).await
145 }
146
147 pub async fn read_bytes(&self) -> Result<Vec<u8>, FileError> {
148 tokio::fs::read(self.path())
149 .await
150 .map_err(|source| FileError::at("read staged blob", self.path(), source))
151 }
152
153 pub fn path_for_atomic_replacement(&mut self) -> &Path {
157 self.staged
158 .as_mut()
159 .expect("atomic stage is unpublished")
160 .close();
161 self.path()
162 }
163
164 pub async fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), FileError> {
165 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
166
167 let staged = self.staged.as_mut().expect("atomic stage is unpublished");
168 let path = staged.path.clone();
169 let file = staged.file_mut();
170 file.set_len(0)
171 .await
172 .map_err(|source| FileError::at("truncate staged blob", &path, source))?;
173 file.seek(std::io::SeekFrom::Start(0))
174 .await
175 .map_err(|source| FileError::at("seek staged blob", &path, source))?;
176 file.write_all(bytes)
177 .await
178 .map_err(|source| FileError::at("write staged blob", &path, source))?;
179 self.file_sync
183 .finish_async_write(file)
184 .await
185 .map_err(|source| FileError::at("finish staged blob write", path, source))
186 }
187
188 pub async fn write_plaintext<R: PlaintextChunkReader>(
192 &mut self,
193 source: &mut R,
194 ) -> Result<u64, StreamWriteError<R::Error>> {
195 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
196
197 let staged = self.staged.as_mut().expect("atomic stage is unpublished");
198 let path = staged.path.clone();
199 let file = staged.file_mut();
200 file.set_len(0).await.map_err(|source| {
201 StreamWriteError::Local(FileError::at("truncate staged blob", &path, source))
202 })?;
203 file.seek(std::io::SeekFrom::Start(0))
204 .await
205 .map_err(|source| {
206 StreamWriteError::Local(FileError::at("seek staged blob", &path, source))
207 })?;
208 let mut written = 0u64;
209 loop {
210 let chunk = source
211 .next_chunk(1 << 20)
212 .await
213 .map_err(StreamWriteError::Source)?;
214 if chunk.is_empty() {
215 break;
216 }
217 file.write_all(&chunk).await.map_err(|source| {
218 StreamWriteError::Local(FileError::at("write staged blob", &path, source))
219 })?;
220 written += chunk.len() as u64;
221 }
222 self.file_sync
226 .finish_async_write(file)
227 .await
228 .map_err(|source| {
229 StreamWriteError::Local(FileError::at("finish staged blob write", path, source))
230 })?;
231 Ok(written)
232 }
233
234 pub async fn write_byte_stream<E: Send>(
235 mut self,
236 mut stream: Pin<Box<dyn Stream<Item = Result<Bytes, E>> + Send>>,
237 ) -> Result<(Self, u64), ByteStreamWriteError<E>> {
238 use tokio::io::{AsyncSeekExt, AsyncWriteExt};
239
240 let write = async {
241 let staged = self.staged.as_mut().expect("atomic stage is unpublished");
242 let path = staged.path.clone();
243 let file = staged.file_mut();
244 file.set_len(0).await.map_err(|source| {
245 AtomicChunkWriteError::Local(FileError::at("truncate staged blob", &path, source))
246 })?;
247 file.seek(std::io::SeekFrom::Start(0))
248 .await
249 .map_err(|source| {
250 AtomicChunkWriteError::Local(FileError::at("seek staged blob", &path, source))
251 })?;
252 let mut written = 0u64;
253 while let Some(chunk) = stream
254 .next()
255 .await
256 .transpose()
257 .map_err(AtomicChunkWriteError::Source)?
258 {
259 file.write_all(&chunk).await.map_err(|source| {
260 AtomicChunkWriteError::Local(FileError::at("write staged blob", &path, source))
261 })?;
262 written += chunk.len() as u64;
263 }
264 self.file_sync
268 .finish_async_write(file)
269 .await
270 .map_err(|source| {
271 AtomicChunkWriteError::Local(FileError::at(
272 "finish staged blob write",
273 path,
274 source,
275 ))
276 })?;
277 Ok(written)
278 }
279 .await;
280 match write {
281 Ok(written) => Ok((self, written)),
282 Err(AtomicChunkWriteError::Source(source)) => match self.take_stage().cleanup().await {
283 Ok(()) => Err(ByteStreamWriteError::Source(source)),
284 Err(cleanup) => Err(ByteStreamWriteError::SourceCleanup { source, cleanup }),
285 },
286 Err(AtomicChunkWriteError::Local(operation)) => {
287 let error = self
288 .take_stage()
289 .fail::<()>(operation)
290 .await
291 .expect_err("failed staged write returns an error");
292 Err(ByteStreamWriteError::Local(error))
293 }
294 }
295 }
296
297 pub async fn copy_from(self, source: &Path) -> Result<(Self, u64, [u8; 32]), FileError> {
300 let input = tokio::fs::File::open(source)
301 .await
302 .map_err(|error| FileError::at("open copy source", source, error))?;
303 self.write_open_file_with_facts(input, source).await
304 }
305
306 async fn write_open_file_with_facts(
307 mut self,
308 mut input: tokio::fs::File,
309 source: &Path,
310 ) -> Result<(Self, u64, [u8; 32]), FileError> {
311 use sha2::{Digest, Sha256};
312 use tokio::io::AsyncWriteExt;
313
314 let copy = async {
315 let staged = self.staged.as_mut().expect("atomic stage is unpublished");
316 let mut buffer = vec![0u8; 1 << 20];
317 let mut size = 0_u64;
318 let mut hasher = Sha256::new();
319 loop {
320 let read = input
321 .read(&mut buffer)
322 .await
323 .map_err(|error| FileError::at("read copy source", source, error))?;
324 if read == 0 {
325 break;
326 }
327 size = size
328 .checked_add(read as u64)
329 .ok_or_else(|| FileError::SizeOverflow {
330 subject: "copy source",
331 path: source.to_path_buf(),
332 })?;
333 hasher.update(&buffer[..read]);
334 staged
335 .file_mut()
336 .write_all(&buffer[..read])
337 .await
338 .map_err(|error| FileError::at("write copy stage", &staged.path, error))?;
339 }
340 self.file_sync
344 .finish_async_write(staged.file_mut())
345 .await
346 .map_err(|error| FileError::at("finish copy stage", &staged.path, error))?;
347 Ok::<_, FileError>((size, hasher.finalize().into()))
348 }
349 .await;
350 match copy {
351 Ok((size, hash)) => Ok((self, size, hash)),
352 Err(operation) => self.take_stage().fail(operation).await,
353 }
354 }
355
356 pub async fn commit(self) -> Result<(), FileError> {
357 let file_sync = self.file_sync.clone();
358 self.commit_with_sync(|path| {
359 let path = path.to_path_buf();
360 async move { file_sync.sync_parent(&path).await }
361 })
362 .await
363 }
364
365 async fn commit_with_sync<F, Fut>(mut self, sync_committed_parent: F) -> Result<(), FileError>
366 where
367 F: FnOnce(&Path) -> Fut,
368 Fut: std::future::Future<Output = Result<(), FileError>>,
369 {
370 let mut staged = self.take_stage();
371 staged.close();
372 let result = async {
373 tokio::fs::rename(&staged.path, &self.destination)
374 .await
375 .map_err(|source| {
376 FileError::between(
377 "rename verified blob",
378 &staged.path,
379 &self.destination,
380 source,
381 )
382 })?;
383 if let Err(operation) = sync_committed_parent(&self.destination).await {
384 if let Err(source) = tokio::fs::rename(&self.destination, &staged.path).await {
385 return Err(FileError::rollback(
386 operation,
387 FileError::between(
388 "rollback verified blob rename",
389 &self.destination,
390 &staged.path,
391 source,
392 ),
393 ));
394 }
395 if let Err(rollback) = self.file_sync.sync_parent(&staged.path).await {
396 return Err(FileError::rollback(operation, rollback));
397 }
398 return Err(operation);
399 }
400 Ok(())
401 }
402 .await;
403 match result {
404 Ok(()) => {
405 staged.disarm();
406 Ok(())
407 }
408 Err(operation) => staged.fail(operation).await,
409 }
410 }
411
412 pub async fn commit_new(self) -> Result<(), CommitNewFileError> {
416 let file_sync = self.file_sync.clone();
417 self.commit_new_with_sync(|path| {
418 let path = path.to_path_buf();
419 let file_sync = file_sync.clone();
420 async move { file_sync.sync_parent(&path).await }
421 })
422 .await
423 }
424
425 async fn commit_new_with_sync<F, Fut>(
426 mut self,
427 mut sync_committed_parent: F,
428 ) -> Result<(), CommitNewFileError>
429 where
430 F: FnMut(&Path) -> Fut,
431 Fut: std::future::Future<Output = Result<(), FileError>>,
432 {
433 let mut staged = self.take_stage();
434 staged.close();
435 match staged.rename_noreplace(&self.destination) {
436 Ok(()) => {}
437 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
438 let operation = CommitNewFileError::DestinationExists(self.destination.clone());
439 return match staged.cleanup().await {
440 Ok(()) => Err(operation),
441 Err(cleanup) => Err(CommitNewFileError::rollback(operation, cleanup)),
442 };
443 }
444 Err(source) => {
445 let operation = FileError::between(
446 "rename verified blob without replacement",
447 &staged.path,
448 &self.destination,
449 source,
450 );
451 return match staged.cleanup().await {
452 Ok(()) => Err(operation.into()),
453 Err(cleanup) => Err(CommitNewFileError::rollback(operation.into(), cleanup)),
454 };
455 }
456 }
457
458 if let Err(operation) = sync_committed_parent(&self.destination).await {
459 return match self.rollback_new_destination().await {
460 Ok(()) => Err(operation.into()),
461 Err(rollback) => Err(CommitNewFileError::rollback(operation.into(), rollback)),
462 };
463 }
464 Ok(())
465 }
466
467 pub async fn discard(mut self) -> Result<(), FileError> {
468 self.take_stage().cleanup().await
469 }
470
471 pub fn discard_blocking(mut self) -> Result<(), FileError> {
472 self.take_stage().cleanup_blocking()
473 }
474
475 pub fn publish_for_transaction(mut self) -> Result<PublishedAtomicFile, FileError> {
476 let staged = self.take_stage();
477 staged.publish_blocking(&self.destination, &self.file_sync)?;
478 Ok(PublishedAtomicFile {
479 destination: self.destination.clone(),
480 file_sync: self.file_sync.clone(),
481 })
482 }
483
484 fn take_stage(&mut self) -> AtomicTempFile {
485 self.staged.take().expect("atomic stage is unpublished")
486 }
487
488 async fn rollback_new_destination(&self) -> Result<(), FileError> {
489 tokio::fs::remove_file(&self.destination)
490 .await
491 .map_err(|source| FileError::at("remove new destination", &self.destination, source))?;
492 self.file_sync.sync_parent(&self.destination).await
493 }
494
495 #[cfg(any(test, feature = "test-utils"))]
496 pub async fn write_for_test(destination: &Path, bytes: &[u8]) -> Result<(), FileError> {
497 let mut staged = Self::create_with_file_sync(destination, FileSync::Disabled).await?;
498 staged.write_bytes(bytes).await?;
499 staged.commit().await
500 }
501
502 #[cfg(any(test, feature = "test-utils"))]
503 pub fn leave_unpublished_for_test(mut self) -> PathBuf {
504 let mut staged = self.take_stage();
505 let path = staged.path.clone();
506 staged.disarm();
507 path
508 }
509}
510
511impl Drop for AtomicStagedFile {
512 fn drop(&mut self) {
513 self.staged.take();
516 }
517}
518
519impl PublishedAtomicFile {
520 pub fn rollback(self) -> Result<(), FileError> {
521 match std::fs::remove_file(&self.destination) {
522 Ok(()) => {}
523 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
524 Err(source) => {
525 return Err(FileError::at(
526 "remove published file",
527 &self.destination,
528 source,
529 ));
530 }
531 }
532 self.file_sync.sync_parent_blocking(&self.destination)
533 }
534}
535
536impl AtomicTempFile {
537 fn create_in(parent: &Path) -> Result<Self, FileError> {
538 let named = tempfile::Builder::new()
539 .prefix(TEMP_BLOB_PREFIX)
540 .tempfile_in(parent)
541 .map_err(|source| FileError::at("create temporary blob", parent, source))?;
542 let (file, path) = named.into_parts();
543 let path = path
544 .keep()
545 .map_err(|source| FileError::at("retain temporary blob path", parent, source.error))?;
546 Ok(Self {
547 path,
548 file: Some(tokio::fs::File::from_std(file)),
549 armed: true,
550 })
551 }
552
553 fn file_mut(&mut self) -> &mut tokio::fs::File {
554 self.file.as_mut().expect("atomic temp file is open")
555 }
556
557 fn close(&mut self) {
558 self.file.take();
559 }
560
561 #[cfg(any(
562 target_os = "android",
563 target_os = "linux",
564 target_os = "macos",
565 target_os = "ios",
566 target_os = "tvos",
567 target_os = "visionos",
568 target_os = "watchos",
569 target_os = "redox",
570 ))]
571 fn rename_noreplace(&mut self, destination: &Path) -> Result<(), std::io::Error> {
572 use rustix::fs::{renameat_with, RenameFlags, CWD};
573
574 self.close();
575 renameat_with(CWD, &self.path, CWD, destination, RenameFlags::NOREPLACE)?;
576 self.armed = false;
577 Ok(())
578 }
579
580 #[cfg(target_os = "windows")]
581 fn rename_noreplace(&mut self, destination: &Path) -> Result<(), std::io::Error> {
582 self.close();
583 let path = tempfile::TempPath::try_from_path(self.path.clone())?;
584 match path.persist_noclobber(destination) {
585 Ok(()) => {
586 self.armed = false;
587 Ok(())
588 }
589 Err(error) => {
590 let source = error.error;
591 let mut path = error.path;
595 path.disable_cleanup(true);
596 Err(source)
597 }
598 }
599 }
600
601 fn disarm(&mut self) {
602 self.close();
603 self.armed = false;
604 }
605
606 async fn cleanup(mut self) -> Result<(), FileError> {
607 self.close();
608 let cleanup = match tokio::fs::remove_file(&self.path).await {
609 Ok(()) => Ok(()),
610 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
611 Err(source) => Err(FileError::at("remove temporary blob", &self.path, source)),
612 };
613 self.armed = false;
614 cleanup
615 }
616
617 fn cleanup_blocking(mut self) -> Result<(), FileError> {
618 self.close();
619 let cleanup = match std::fs::remove_file(&self.path) {
620 Ok(()) => Ok(()),
621 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
622 Err(source) => Err(FileError::at("remove temporary blob", &self.path, source)),
623 };
624 self.armed = false;
625 cleanup
626 }
627
628 async fn fail<T>(self, operation: FileError) -> Result<T, FileError> {
629 match self.cleanup().await {
630 Ok(()) => Err(operation),
631 Err(cleanup) => Err(FileError::rollback(operation, cleanup)),
632 }
633 }
634
635 fn publish_blocking(
636 mut self,
637 destination: &Path,
638 file_sync: &FileSync,
639 ) -> Result<(), FileError> {
640 self.close();
641 let operation = match std::fs::rename(&self.path, destination) {
642 Ok(()) => match file_sync.sync_parent_blocking(destination) {
643 Ok(()) => {
644 self.armed = false;
645 return Ok(());
646 }
647 Err(operation) => {
648 let rollback = std::fs::rename(destination, &self.path)
649 .map_err(|source| {
650 FileError::between(
651 "rollback published file rename",
652 destination,
653 &self.path,
654 source,
655 )
656 })
657 .and_then(|()| file_sync.sync_parent_blocking(&self.path));
658 match rollback {
659 Ok(()) => operation,
660 Err(rollback) => FileError::rollback(operation, rollback),
661 }
662 }
663 },
664 Err(source) => {
665 FileError::between("rename temporary blob", &self.path, destination, source)
666 }
667 };
668 match self.cleanup_blocking() {
669 Ok(()) => Err(operation),
670 Err(cleanup) => Err(FileError::rollback(operation, cleanup)),
671 }
672 }
673}
674
675impl Drop for AtomicTempFile {
676 fn drop(&mut self) {
677 self.file.take();
678 if !self.armed {
679 return;
680 }
681 match std::fs::remove_file(&self.path) {
682 Ok(()) => {}
683 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
684 Err(error) => {
685 tracing::warn!(
686 path = %self.path.display(),
687 %error,
688 "could not remove canceled atomic-write temp blob"
689 );
690 }
691 }
692 }
693}
694
695pub async fn file_facts(path: &Path) -> Result<(u64, [u8; 32]), FileError> {
699 let (_, size, digest) =
700 read_selected_with_facts(path, ExactReadSelection::IdentityOnly).await?;
701 Ok((size, digest))
702}
703
704pub async fn file_facts_with_progress(
707 path: &Path,
708 progress: impl Fn(u64) + Send + Sync,
709) -> Result<(u64, [u8; 32]), FileError> {
710 let (_, size, digest) =
711 read_selected_with_facts_and_progress(path, ExactReadSelection::IdentityOnly, &progress)
712 .await?;
713 Ok((size, digest))
714}
715
716pub async fn file_len(path: &Path) -> Result<u64, FileError> {
717 tokio::fs::metadata(path)
718 .await
719 .map(|metadata| metadata.len())
720 .map_err(|source| FileError::at("stat local blob", path, source))
721}
722
723#[derive(Clone, Copy)]
724enum ExactReadSelection {
725 IdentityOnly,
726 #[cfg(test)]
727 Whole,
728}
729
730async fn read_selected_with_facts(
731 path: &Path,
732 selection: ExactReadSelection,
733) -> Result<(Vec<u8>, u64, [u8; 32]), FileError> {
734 read_selected_with_facts_and_progress(path, selection, &|_| {}).await
735}
736
737async fn read_selected_with_facts_and_progress(
738 path: &Path,
739 selection: ExactReadSelection,
740 progress: &(dyn Fn(u64) + Sync),
741) -> Result<(Vec<u8>, u64, [u8; 32]), FileError> {
742 let mut file = tokio::fs::File::open(path)
743 .await
744 .map_err(|source| FileError::at("open exact file", path, source))?;
745 read_open_file_with_facts_and_progress(&mut file, path, selection, progress).await
746}
747
748#[cfg(test)]
749async fn read_open_file_with_facts(
750 file: &mut tokio::fs::File,
751 path: &Path,
752 selection: ExactReadSelection,
753) -> Result<(Vec<u8>, u64, [u8; 32]), FileError> {
754 read_open_file_with_facts_and_progress(file, path, selection, &|_| {}).await
755}
756
757async fn read_open_file_with_facts_and_progress(
758 file: &mut tokio::fs::File,
759 path: &Path,
760 selection: ExactReadSelection,
761 progress: &(dyn Fn(u64) + Sync),
762) -> Result<(Vec<u8>, u64, [u8; 32]), FileError> {
763 use sha2::{Digest, Sha256};
764
765 #[cfg(test)]
766 let mut selected = Vec::new();
767 #[cfg(not(test))]
768 let selected = Vec::new();
769 let mut size = 0_u64;
770 let mut hasher = Sha256::new();
771 let mut buffer = vec![0_u8; 1 << 20];
772 loop {
773 let read = file
774 .read(&mut buffer)
775 .await
776 .map_err(|source| FileError::at("read exact file", path, source))?;
777 if read == 0 {
778 break;
779 }
780 size = size
781 .checked_add(read as u64)
782 .ok_or_else(|| FileError::SizeOverflow {
783 subject: "exact file",
784 path: path.to_path_buf(),
785 })?;
786 hasher.update(&buffer[..read]);
787 progress(size);
788 match selection {
789 ExactReadSelection::IdentityOnly => {}
790 #[cfg(test)]
791 ExactReadSelection::Whole => selected.extend_from_slice(&buffer[..read]),
792 }
793 }
794 Ok((selected, size, hasher.finalize().into()))
795}
796
797pub struct OpenFile {
813 file: tokio::sync::Mutex<tokio::fs::File>,
814 path: PathBuf,
815 size: u64,
816}
817
818impl OpenFile {
819 pub async fn open(path: &Path) -> Result<Self, FileError> {
821 let file = tokio::fs::File::open(path)
822 .await
823 .map_err(|source| FileError::at("open local file", path, source))?;
824 let size = file
825 .metadata()
826 .await
827 .map_err(|source| FileError::at("stat local file", path, source))?
828 .len();
829 Ok(Self {
830 file: tokio::sync::Mutex::new(file),
831 path: path.to_path_buf(),
832 size,
833 })
834 }
835
836 pub fn size(&self) -> u64 {
837 self.size
838 }
839
840 pub async fn read_at(&self, offset: u64, len: u64) -> Result<Vec<u8>, FileError> {
844 use tokio::io::AsyncSeekExt;
845
846 if len == 0 {
847 return Ok(Vec::new());
848 }
849 let mut buffer =
850 vec![0_u8; usize::try_from(len).map_err(|_| FileError::RangeTooLarge { len })?];
851 let mut file = self.file.lock().await;
852 file.seek(std::io::SeekFrom::Start(offset))
853 .await
854 .map_err(|source| FileError::at("seek local file range", &self.path, source))?;
855 file.read_exact(&mut buffer)
856 .await
857 .map_err(|source| FileError::at("read local file range", &self.path, source))?;
858 Ok(buffer)
859 }
860}
861
862#[cfg(test)]
863pub(crate) async fn read(path: &Path) -> Result<Vec<u8>, FileError> {
864 tokio::fs::read(path)
865 .await
866 .map_err(|source| FileError::at("read local blob", path, source))
867}
868
869#[cfg(test)]
870pub(crate) async fn exists(path: &Path) -> Result<bool, FileError> {
871 tokio::fs::try_exists(path)
872 .await
873 .map_err(|source| FileError::at("check local blob", path, source))
874}
875
876#[cfg(test)]
877pub(crate) async fn rename(from: &Path, to: &Path) -> Result<(), FileError> {
878 tokio::fs::rename(from, to)
879 .await
880 .map_err(|source| FileError::between("rename file", from, to, source))
881}
882
883#[cfg(test)]
884pub(crate) async fn remove_file(path: &Path) -> Result<bool, FileError> {
885 match tokio::fs::remove_file(path).await {
886 Ok(()) => Ok(true),
887 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
888 Err(source) => Err(FileError::at("remove file", path, source)),
889 }
890}
891
892#[cfg(test)]
893#[path = "local_file_tests.rs"]
894mod tests;