1use super::FilesMode;
4use crate::{
5 files::file_failure::stored,
6 files::file_status::Uploading,
7 files::{Files, FilesInner},
8 FileReadError,
9};
10use coven_crypto::{ContentHash, ContentHasher, FileKey};
11use coven_database::{
12 CacheReservation, DbError, FileLocation, FilePlace, FileRef, LocalFileStream, TransferEnd,
13 TransferMeter,
14};
15use coven_database::{FileLocationKey, Writer};
16use coven_format::{
17 chunks::CHUNK_SIZE,
18 file::{FileHeader, FILE_HEADER_LEN},
19};
20use coven_foundation::files::StoreDirAccess;
21use coven_foundation::files::{FileName, StoreReadLock};
22use coven_foundation::locks::read_unpoisoned;
23use coven_storage::{ByteProgress, ByteRange, ObjectPath};
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::Arc;
26
27use super::file_error::TransferError;
28
29#[path = "file_read_partial.rs"]
30mod partial;
31#[path = "file_read_parts.rs"]
32mod parts;
33#[path = "file_read_whole.rs"]
34mod whole;
35
36const REQUEST_BYTES: u64 = 1024 * 1024;
37pub struct FileStream<M: FilesMode = Writer> {
42 id: String,
43 source: OpenFile<M>,
44 state: tokio::sync::Mutex<FileStreamState>,
45}
46enum OpenFile<M: FilesMode> {
47 Local {
48 stream: LocalFileStream,
49 owner: Arc<FilesInner<M>>,
50 file: FileRef,
51 uploading: Option<Arc<Uploading>>,
55 },
56 Uploaded(Arc<UploadedFile<M>>),
57}
58struct FileStreamState {
59 end: Option<u64>,
60 hash: Option<ContentHasher>,
61 ahead: Option<coven_foundation::task::OwnedTask>,
62}
63impl Drop for FileStreamState {
64 fn drop(&mut self) {
65 if let Some(task) = &self.ahead {
66 task.abort();
67 }
68 }
69}
70pub(crate) struct UploadedFile<M: FilesMode = Writer> {
71 owner: Arc<FilesInner<M>>,
72 file: FileRef,
73 location: FileLocation,
75 hash: ContentHash,
77 header: FileHeader,
80 header_checked: std::sync::atomic::AtomicBool,
83 key: FileKey,
84 path: ObjectPath,
85 parts: std::sync::OnceLock<Option<parts::HeldParts>>,
89 _lock: StoreReadLock,
90}
91#[derive(Clone)]
96pub struct ReadProgress {
97 report: Arc<dyn Fn(u64) + Send + Sync>,
98}
99impl std::fmt::Debug for ReadProgress {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("ReadProgress").finish_non_exhaustive()
102 }
103}
104impl ReadProgress {
105 pub fn new(report: impl Fn(u64) + Send + Sync + 'static) -> Self {
107 Self { report: Arc::new(report) }
108 }
109 fn tell(&self, here: u64) {
110 (self.report)(here);
111 }
112}
113struct RangeHere<'a> {
116 progress: &'a ReadProgress,
117 offset: u64,
118 len: u64,
119 here: u64,
120}
121enum ChunkDestination<'a> {
122 Cache(Option<RangeHere<'a>>),
124 WholeFile { progress: &'a Arc<whole::WholeProgress>, fetched: &'a Arc<AtomicU64> },
127}
128impl<M: FilesMode> Files<M> {
129 pub async fn open_file_stream(&self, file: &FileRef) -> Result<FileStream<M>, FileReadError> {
133 self.inner.check_open()?;
134 let mut file = self.inner.current(file).await?;
135 if let FilePlace::DeviceOnly { .. } = file.place() {
136 let uploading = self.inner.local(&file).await?.map(Arc::new);
137 let local = self.inner.database.open_local(&file).await.map_err(FileReadError::from);
138 match local {
139 Ok(Some(stream)) => {
140 return Ok(FileStream::new(
141 &file,
142 OpenFile::Local {
143 stream,
144 owner: self.inner.clone(),
145 file: file.clone(),
146 uploading,
147 },
148 ))
149 }
150 Ok(None) => file = self.inner.current(&file).await?,
154 Err(error) => {
155 return match &uploading {
156 Some(uploading) => {
157 self.inner.complete_local_read(file.id(), uploading, Err(error)).await
158 }
159 None => Err(error),
160 }
161 }
162 }
163 }
164 if let FilePlace::DeviceOnly { device } = file.place() {
165 return Err(self.inner.unreadable(&file, device).await);
166 }
167 let opened = match UploadedFile::open(self.inner.clone(), file.clone()).await {
168 Ok(opened) => opened,
169 Err(error) => return self.inner.complete_read(&file, Err(error.into()), false).await,
170 };
171 Ok(FileStream::new(&file, OpenFile::Uploaded(Arc::new(opened))))
172 }
173 pub async fn read_file(&self, file: &FileRef) -> Result<Vec<u8>, FileReadError> {
176 self.open_file_stream(file).await?.read_at(0, file.size()).await
177 }
178}
179impl<M: FilesMode> FileStream<M> {
180 fn new(file: &FileRef, source: OpenFile<M>) -> Self {
181 Self {
182 id: file.id().to_string(),
183 source,
184 state: tokio::sync::Mutex::new(FileStreamState {
185 end: Some(0),
186 hash: Some(ContentHasher::new()),
187 ahead: None,
188 }),
189 }
190 }
191
192 pub fn plaintext_size(&self) -> u64 {
194 match &self.source {
195 OpenFile::Local { stream, .. } => stream.plaintext_size(),
196 OpenFile::Uploaded(file) => file.header.size(),
197 }
198 }
199 pub async fn read_at(&self, offset: u64, len: u64) -> Result<Vec<u8>, FileReadError> {
203 self.read(offset, len, None).await
204 }
205 pub async fn read_at_reporting(
209 &self,
210 offset: u64,
211 len: u64,
212 progress: &ReadProgress,
213 ) -> Result<Vec<u8>, FileReadError> {
214 self.read(offset, len, Some(progress)).await
215 }
216 async fn read(
217 &self,
218 offset: u64,
219 len: u64,
220 progress: Option<&ReadProgress>,
221 ) -> Result<Vec<u8>, FileReadError> {
222 match &self.source {
223 OpenFile::Local { stream, owner, file, uploading } => {
224 let read = stream.read_at(offset, len).await.map_err(FileReadError::from);
225 match uploading {
226 Some(uploading) => owner.complete_local_read(file.id(), uploading, read).await,
227 None => read,
228 }
229 }
230 OpenFile::Uploaded(file) => {
231 file.check_range(offset, len)?;
232 let mut state = self.state.lock().await;
233 let sequential = state.end == Some(offset);
234 if !sequential {
235 if let Some(task) = state.ahead.take() {
236 task.abort();
237 }
238 }
239 if offset == 0 {
240 state.hash = Some(ContentHasher::new());
241 } else if !sequential {
242 state.hash = None;
243 }
244 let read = file.range(offset, len, progress).await;
245 let bytes = match read {
246 Ok(bytes) => bytes,
247 Err(error) => {
248 return file.owner.complete_read(&file.file, Err(error.into()), false).await
249 }
250 };
251 let mut whole = false;
252 if let Some(hash) = &mut state.hash {
253 hash.update(&bytes);
254 }
255 if offset + len == file.header.size() {
256 if let Some(hash) = state.hash.take() {
257 if hash.finish() != file.hash {
258 return file
259 .owner
260 .complete_read(
261 &file.file,
262 Err(TransferError::Refused(crate::Refusal::ContentHash)),
263 false,
264 )
265 .await;
266 }
267 whole = true;
268 }
269 }
270 state.end = Some(offset + len);
271 if sequential && offset > 0 && offset + len < file.header.size() {
272 if let Some(task) = state.ahead.take() {
273 task.abort();
274 }
275 let ahead = file.clone();
276 let start = offset + len;
277 state.ahead = Some(coven_foundation::task::spawn_owned(
280 "a read-ahead",
281 file.owner.database.bugs().clone(),
282 async move {
283 let length = (ahead.header.size() - start).min(REQUEST_BYTES);
284 let result =
285 ahead.range(start, length, None).await.map_err(TransferError::from);
286 ahead.owner.finish_read_ahead(&ahead.file, result).await;
287 },
288 |_| async {},
289 ));
290 }
291 file.owner.complete_read(&file.file, Ok(bytes), whole).await
292 }
293 }
294 }
295 pub fn read_range(
300 &self,
301 offset: u64,
302 len: u64,
303 ) -> Result<FileRangeStream<'_, M>, FileReadError> {
304 if offset.checked_add(len).is_none_or(|end| end > self.plaintext_size()) {
305 return Err(FileReadError::RangeOutOfBounds {
306 id: self.id.clone(),
307 offset,
308 end: offset.saturating_add(len),
309 size: self.plaintext_size(),
310 });
311 }
312 Ok(FileRangeStream { file: self, offset, remaining: len, empty: len == 0 })
313 }
314}
315pub struct FileRangeStream<'a, M: FilesMode = Writer> {
317 file: &'a FileStream<M>,
318 offset: u64,
319 remaining: u64,
320 empty: bool,
321}
322impl<M: FilesMode> FileRangeStream<'_, M> {
323 pub async fn next(&mut self) -> Result<Option<Vec<u8>>, FileReadError> {
325 if self.empty {
326 self.empty = false;
327 self.file.read_at(self.offset, self.remaining).await?;
328 self.remaining = 0;
329 return Ok(None);
330 }
331 if self.remaining == 0 {
332 return Ok(None);
333 }
334 let length = self.remaining.min(REQUEST_BYTES - CHUNK_SIZE as u64);
335 let bytes = self.file.read_at(self.offset, length).await?;
336 self.offset += length;
337 self.remaining -= length;
338 Ok(Some(bytes))
339 }
340}
341impl FilesInner {
342 pub(crate) async fn download_whole(
346 self: &Arc<Self>,
347 file: FileRef,
348 ) -> Result<Option<CacheReservation>, TransferError> {
349 if file.size() == 0 && self.downloads_held() {
351 return Ok(None);
352 }
353 UploadedFile::open(self.clone(), file).await?.keep_whole().await
354 }
355}
356impl<M: FilesMode> UploadedFile<M> {
357 pub(crate) async fn open(
363 owner: Arc<FilesInner<M>>,
364 file: FileRef,
365 ) -> Result<Self, FileReadError> {
366 let (location, hash) = stored(&file)?;
367 let location = location.clone();
368 let directory = owner.directory.clone();
369 let lock = coven_foundation::task::blocking_completing(
370 "lock the store for reading",
371 owner.database.bugs().clone(),
372 move || directory.lock_read_only(),
373 )
374 .await
375 .map_err(coven_foundation::files::FileError::from)??;
376 let coven_format::file_reference::FileReference { device, object, key } =
377 location.reference()?;
378 let path = ObjectPath::file(owner.directory.id(), device, object);
379 let opened = Self {
380 header: FileHeader::new(file.size()),
381 header_checked: std::sync::atomic::AtomicBool::new(false),
382 owner,
383 file,
384 location,
385 hash,
386 key,
387 path,
388 parts: std::sync::OnceLock::new(),
389 _lock: lock,
390 };
391 if opened.header.chunk_count() == 0 {
392 let _file = opened.owner.lock_file(&opened.file, &opened.location).await;
393 let _guard = opened.owner.cache.read().await;
394 let cached = opened.cached_header().await?;
395 if cached != CachedHeader::Checked {
396 opened.fetch_header(cached).await?;
397 }
398 }
399 Ok(opened)
400 }
401 async fn cached_header(&self) -> Result<CachedHeader, FileReadError> {
403 if self.header_checked.load(std::sync::atomic::Ordering::Acquire) {
404 return Ok(CachedHeader::Checked);
405 }
406 let Some(bytes) = self.owner.database.cached(&self.file, -1).await? else {
407 return Ok(CachedHeader::Missing);
408 };
409 Ok(match self.accept_header(&bytes) {
412 Ok(()) => CachedHeader::Checked,
413 Err(_) => CachedHeader::Damaged,
414 })
415 }
416 async fn fetch_header(&self, cached: CachedHeader) -> Result<(), FileReadError> {
419 let bytes = self.fetch(0, FILE_HEADER_LEN as u64, &ByteProgress::unread()).await?;
420 if let Err(refused) = self.accept_header(&bytes) {
421 return Err(self.confirm(0, &bytes, refused).await);
422 }
423 match cached {
424 CachedHeader::Damaged => {
425 Ok(self.owner.database.repair_cached(&self.file, -1, bytes).await?)
426 }
427 CachedHeader::Checked | CachedHeader::Missing => self.publish_header(bytes).await,
428 }
429 }
430 async fn confirm(&self, start: u64, first: &[u8], refused: FileReadError) -> FileReadError {
436 if !matches!(refused, FileReadError::Integrity { .. }) {
437 return refused;
438 }
439 let end = start + first.len() as u64;
440 if let Some(Some(parts)) = self.parts.get() {
443 parts.forget(start, end);
444 }
445 match self.fetch(start, end, &ByteProgress::unread()).await {
446 Ok(again) if again == first => refused,
447 Ok(_) => coven_storage::StorageFailure::Corrupted
448 .with_source("a second fetch of the file returned other bytes")
449 .into(),
450 Err(error) => error,
451 }
452 }
453 async fn publish_header(&self, bytes: Vec<u8>) -> Result<(), FileReadError> {
455 self.accept_header(&bytes)?;
456 cache(&self.owner, &self.file, -1, 1, bytes).await?;
457 self.owner.trim(self.file.namespace()).await;
458 Ok(())
459 }
460 fn accept_header(&self, bytes: &[u8]) -> Result<(), FileReadError> {
463 let header = FileHeader::decode(bytes).map_err(|error| match error {
464 coven_format::Error::UnsupportedVersion(version)
465 if version > coven_format::FORMAT_VERSION =>
466 {
467 FileReadError::UpdateRequired(crate::RequiredUpdate::CovenFormat { version })
468 }
469 _ => self.integrity(),
470 })?;
471 if header != self.header {
472 return Err(self.integrity());
473 }
474 self.header_checked.store(true, std::sync::atomic::Ordering::Release);
475 Ok(())
476 }
477 fn integrity(&self) -> FileReadError {
478 FileReadError::Integrity { id: self.file.id().to_string(), cause: None }
479 }
480 fn check_range(&self, offset: u64, len: u64) -> Result<(), FileReadError> {
481 if offset.checked_add(len).is_none_or(|end| end > self.header.size()) {
482 Err(FileReadError::RangeOutOfBounds {
483 id: self.file.id().to_string(),
484 offset,
485 end: offset.saturating_add(len),
486 size: self.header.size(),
487 })
488 } else {
489 Ok(())
490 }
491 }
492 #[expect(
493 clippy::map_err_ignore,
494 reason = "a size past the target integer is TooLarge, which names the field and size"
495 )]
496 pub(crate) async fn range(
497 &self,
498 offset: u64,
499 len: u64,
500 progress: Option<&ReadProgress>,
501 ) -> Result<Vec<u8>, FileReadError> {
502 self.owner.check_open()?;
503 self.check_range(offset, len)?;
504 let length = usize::try_from(len).map_err(|_| DbError::TooLarge {
505 field: "file range",
506 actual: len,
507 maximum: usize::MAX as u64,
508 })?;
509 let mut result = Vec::new();
510 result.try_reserve_exact(length).map_err(|_| DbError::TooLarge {
511 field: "file range",
512 actual: len,
513 maximum: usize::MAX as u64,
514 })?;
515 if len == 0 {
516 return Ok(result);
517 }
518 let size = CHUNK_SIZE as u64;
519 let mut index = offset / size;
520 let last = (offset + len - 1) / size;
521 while index <= last {
522 let here = result.len() as u64;
523 let destination = ChunkDestination::Cache(progress.map(|progress| RangeHere {
524 progress,
525 offset,
526 len,
527 here,
528 }));
529 let ChunkStep::Read(end, chunks) = self.chunks(index, last, destination).await? else {
530 unreachable!("only a pin's download waits for the network")
531 };
532 for (number, chunk) in chunks.into_iter().enumerate() {
533 let plain = chunk.plaintext;
534 let position = (index + number as u64) * size;
535 let start = offset.saturating_sub(position) as usize;
536 let end = (offset + len - position).min(plain.len() as u64) as usize;
537 result.extend_from_slice(&plain[start..end]);
538 }
539 index = end + 1;
540 }
541 Ok(result)
542 }
543 #[expect(
547 clippy::map_err_ignore,
548 reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
549 )]
550 async fn chunks(
551 &self,
552 first: u64,
553 last: u64,
554 destination: ChunkDestination<'_>,
555 ) -> Result<ChunkStep, FileReadError> {
556 self.owner.check_open()?;
557 let file_lock = self.owner.lock_file(&self.file, &self.location).await;
558 let _guard = self.owner.cache.read().await;
559 let cached = self
562 .owner
563 .database
564 .cached(&self.file, first as i64)
565 .await?
566 .map(|bytes| self.open_chunk(first, bytes));
567 let header = self.cached_header().await?;
568 if matches!(destination, ChunkDestination::WholeFile { .. })
571 && (!matches!(cached, Some(Ok(_))) || header != CachedHeader::Checked)
572 && self.owner.downloads_held()
573 {
574 return Ok(ChunkStep::Held);
575 }
576 let mut with_header = false;
579 if header == CachedHeader::Missing && first == 0 && cached.is_none() {
580 with_header = true;
581 } else if header != CachedHeader::Checked {
582 self.fetch_header(header).await?;
583 }
584 match cached {
585 Some(Ok(chunk)) => return Ok(ChunkStep::Read(first, vec![chunk])),
586 Some(Err(_)) => {
587 return Ok(ChunkStep::Read(
588 first,
589 vec![self.write_back(first, &destination).await?],
590 ))
591 }
592 None => {}
593 }
594 let first_chunk = self.header.chunk(first).map_err(|_| self.integrity())?;
595 let start = first_chunk.offset;
596 let mut end_index = first;
597 let mut stop = None;
602 let mut end = first_chunk.end();
603 while end_index < last {
604 let next = self.header.chunk(end_index + 1).map_err(|_| self.integrity())?;
605 let next_end = next.end();
606 let request_start = if with_header { 0 } else { start };
607 if next_end - request_start > REQUEST_BYTES {
608 break;
609 }
610 if let Some(bytes) =
611 self.owner.database.cached(&self.file, (end_index + 1) as i64).await?
612 {
613 stop = Some(bytes);
614 break;
615 }
616 end_index += 1;
617 end = next_end;
618 }
619 let received = self.received(&destination, first, end_index, start)?;
620 let request_start = if with_header { 0 } else { start };
621 if matches!(destination, ChunkDestination::WholeFile { .. }) && !with_header {
626 drop(file_lock);
627 }
628 let fetched = self.fetch(request_start, end, &received).await?;
629 let chunks_start = if with_header {
632 let header = fetched.get(..FILE_HEADER_LEN).ok_or_else(|| self.integrity())?;
633 if let Err(refused) = self.accept_header(header) {
634 return Err(self.confirm(request_start, &fetched, refused).await);
635 }
636 self.publish_header(header.to_vec()).await?;
637 FILE_HEADER_LEN
638 } else {
639 0
640 };
641 let (bytes, mut plain) = match self.open_fetched(&fetched[chunks_start..], first, end_index)
642 {
643 Ok(opened) => opened,
644 Err(refused) => return Err(self.confirm(request_start, &fetched, refused).await),
645 };
646 if matches!(destination, ChunkDestination::Cache(_)) {
651 cache(&self.owner, &self.file, first as i64, (end_index - first + 1) as i64, bytes)
652 .await?;
653 self.owner.trim(self.file.namespace()).await;
654 }
655 if let Some(Ok(chunk)) = stop.map(|bytes| self.open_chunk(end_index + 1, bytes)) {
658 end_index += 1;
659 plain.push(chunk);
660 }
661 Ok(ChunkStep::Read(end_index, plain))
662 }
663 #[expect(
667 clippy::map_err_ignore,
668 reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
669 )]
670 async fn write_back(
671 &self,
672 index: u64,
673 destination: &ChunkDestination<'_>,
674 ) -> Result<OpenedChunk, FileReadError> {
675 let chunk = self.header.chunk(index).map_err(|_| self.integrity())?;
676 let received = self.received(destination, index, index, chunk.offset)?;
677 let fetched = self.fetch(chunk.offset, chunk.end(), &received).await?;
678 let opened = match self.open_chunk(index, fetched.clone()) {
679 Ok(opened) => opened,
680 Err(refused) => return Err(self.confirm(chunk.offset, &fetched, refused).await),
681 };
682 self.owner.database.repair_cached(&self.file, index as i64, fetched).await?;
683 Ok(opened)
684 }
685 #[expect(
692 clippy::map_err_ignore,
693 reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
694 )]
695 fn received(
696 &self,
697 destination: &ChunkDestination<'_>,
698 first: u64,
699 last: u64,
700 start: u64,
701 ) -> Result<ByteProgress, FileReadError> {
702 match destination {
703 ChunkDestination::WholeFile { progress, fetched } => {
704 let (progress, fetched) = ((*progress).clone(), (*fetched).clone());
705 let furthest = AtomicU64::new(start);
706 Ok(ByteProgress::new(move |reached| {
707 let before = furthest.fetch_max(reached, Ordering::Relaxed);
708 if reached > before {
709 fetched.fetch_add(reached - before, Ordering::Relaxed);
710 progress.arrived(reached - before);
711 }
712 }))
713 }
714 ChunkDestination::Cache(None) => Ok(ByteProgress::unread()),
715 ChunkDestination::Cache(Some(range)) => {
716 let mut chunks = Vec::new();
719 for index in first..=last {
720 let chunk = self.header.chunk(index).map_err(|_| self.integrity())?;
721 let plain_start = chunk.plaintext_start();
722 let from = range.offset.max(plain_start) - plain_start;
723 let to = (range.offset + range.len)
724 .min(plain_start + chunk.plaintext_length as u64)
725 - plain_start;
726 chunks.push((chunk.offset, chunk.plaintext_length as u64, from, to));
727 }
728 let progress = range.progress.clone();
729 let here = range.here;
730 progress.tell(here);
731 Ok(ByteProgress::new(move |reached| {
732 let fetched: u64 = chunks
733 .iter()
734 .map(|&(offset, length, from, to)| {
735 let received = reached.saturating_sub(offset).min(length);
736 received.clamp(from, to) - from
737 })
738 .sum();
739 progress.tell(here + fetched);
740 }))
741 }
742 }
743 }
744 #[expect(
747 clippy::map_err_ignore,
748 reason = "a chunk the file size's layout lacks is the disagreement Integrity names"
749 )]
750 fn open_fetched(
751 &self,
752 fetched: &[u8],
753 first: u64,
754 last: u64,
755 ) -> Result<(Vec<u8>, Vec<OpenedChunk>), FileReadError> {
756 let bytes = fetched.to_vec();
757 let mut plain = Vec::new();
758 let mut cursor = 0;
759 for index in first..=last {
760 let length = self.header.chunk(index).map_err(|_| self.integrity())?.stored_length();
761 let sealed = bytes.get(cursor..cursor + length).ok_or_else(|| self.integrity())?;
762 let opened = self
763 .header
764 .open_chunk(&self.key, self.path.as_str(), index, sealed)
765 .map_err(|error| FileReadError::chunk(self.file.id().to_string(), error))?;
766 plain.push(OpenedChunk { plaintext: opened, ciphertext: sealed.to_vec() });
767 cursor += length;
768 }
769 Ok((bytes, plain))
770 }
771 fn open_chunk(&self, index: u64, bytes: Vec<u8>) -> Result<OpenedChunk, FileReadError> {
773 let plaintext = self
774 .header
775 .open_chunk(&self.key, self.path.as_str(), index, &bytes)
776 .map_err(|error| FileReadError::chunk(self.file.id().to_string(), error))?;
777 Ok(OpenedChunk { plaintext, ciphertext: bytes })
778 }
779}
780
781struct OpenedChunk {
782 plaintext: Vec<u8>,
783 ciphertext: Vec<u8>,
784}
785
786#[derive(Clone, Copy, PartialEq, Eq)]
788enum CachedHeader {
789 Checked,
791 Missing,
793 Damaged,
795}
796
797enum ChunkStep {
799 Read(u64, Vec<OpenedChunk>),
801 Held,
803}
804
805async fn cache<M: FilesMode>(
806 owner: &FilesInner<M>,
807 file: &FileRef,
808 first: i64,
809 chunks: i64,
810 bytes: Vec<u8>,
811) -> Result<(), FileReadError> {
812 let name = FileName::new(owner.ids.new_id().to_string()).expect("UUID cache name");
813 owner.database.cache_range(file, first, chunks, name, bytes).await?;
814 Ok(())
815}
816impl<M: FilesMode> UploadedFile<M> {
817 #[expect(
822 clippy::map_err_ignore,
823 reason = "a size whose encrypted layout overflows is the disagreement Integrity names"
824 )]
825 async fn fetch(
826 &self,
827 start: u64,
828 end: u64,
829 received: &ByteProgress,
830 ) -> Result<Vec<u8>, FileReadError> {
831 let storage =
832 read_unpoisoned(&self.owner.storage).clone().ok_or(FileReadError::NoStorage)?;
833 let size = self.header.encrypted_size().map_err(|_| self.integrity())?;
834 let parts = self
835 .parts
836 .get_or_init(|| storage.read_part_size().map(|part| parts::HeldParts::new(part, size)));
837 let (owner, file, path) = (&self.owner, &self.file, &self.path);
838 match parts {
839 None => fetch(owner, file, path, storage.as_ref(), start, end, received).await,
840 Some(parts) => {
841 parts
842 .read(start, end, received, |from, to, heard| {
843 let storage = storage.clone();
844 async move {
845 fetch(owner, file, path, storage.as_ref(), from, to, &heard).await
846 }
847 })
848 .await
849 }
850 }
851 }
852}
853
854async fn fetch<M: FilesMode>(
859 owner: &FilesInner<M>,
860 file: &FileRef,
861 path: &ObjectPath,
862 storage: &dyn coven_storage::Storage,
863 start: u64,
864 end: u64,
865 received: &ByteProgress,
866) -> Result<Vec<u8>, FileReadError> {
867 let mut stream = match storage
871 .stream_object_reporting(path, Some(ByteRange::new(start, end)?), received)
872 .await
873 {
874 Ok(stream) => stream,
875 Err(error) if error.failure() == coven_storage::StorageFailure::NotFound => {
876 let (location, _) = stored(file)?;
877 return Err(owner.absence_error(file, location));
878 }
879 Err(error) if error.failure() == coven_storage::StorageFailure::InvalidRange => {
882 return Err(FileReadError::Integrity { id: file.id().to_string(), cause: None })
883 }
884 Err(error) => return Err(error.into()),
885 };
886 crate::files::file_status::complete(file, stream.metadata())?;
889 let mut bytes = Vec::with_capacity((end - start) as usize);
890 let mut buffer = vec![0; 64 * 1024];
891 loop {
892 let length = stream.read(&mut buffer).await?;
893 if length == 0 {
894 break;
895 }
896 bytes.extend_from_slice(&buffer[..length]);
897 }
898 if bytes.len() as u64 != end - start {
899 return Err(FileReadError::Integrity { id: file.id().to_string(), cause: None });
900 }
901 Ok(bytes)
902}