Skip to main content

ekv_fs/
file.rs

1//! Streaming read handle for a single open file.
2
3use ekv::{Database, flash::Flash};
4use embassy_sync::blocking_mutex::raw::RawMutex;
5use heapless::String;
6
7use crate::error::Error;
8use crate::key::{self, KEY_BUF_CAP, KEY_SUFFIX_OVERHEAD};
9use crate::meta::FileMeta;
10
11/// Handle for sequential, chunk-cached reads of one file.
12pub struct EkvFile<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize> {
13    db: &'a Database<F, M>,
14    path: String<MAX_PATH_LEN>,
15    meta: FileMeta,
16
17    current_chunk: usize,
18    chunk_offset: usize,
19    cache: [u8; CHUNK_SIZE],
20    cache_valid: bool,
21}
22
23impl<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize>
24    EkvFile<'a, F, M, MAX_PATH_LEN, CHUNK_SIZE>
25{
26    const _KEY_LEN_OK: () = assert!(MAX_PATH_LEN + KEY_SUFFIX_OVERHEAD <= KEY_BUF_CAP);
27
28    pub(crate) fn new(db: &'a Database<F, M>, path: String<MAX_PATH_LEN>, meta: FileMeta) -> Self {
29        Self {
30            db,
31            path,
32            meta,
33            current_chunk: 0,
34            chunk_offset: 0,
35            cache: [0; CHUNK_SIZE],
36            cache_valid: false,
37        }
38    }
39
40    /// File metadata (size and chunk count).
41    pub const fn meta(&self) -> &FileMeta {
42        &self.meta
43    }
44
45    /// Total file size in bytes.
46    pub const fn size(&self) -> usize {
47        self.meta.size
48    }
49
50    /// Reads up to `buf.len()` bytes, advancing an internal offset.
51    ///
52    /// Returns `0` at end-of-file.
53    pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
54        if self.current_chunk >= self.meta.chunks {
55            return Ok(0);
56        }
57
58        if !self.cache_valid {
59            let mut key_buf = String::<KEY_BUF_CAP>::new();
60            key::format_chunk_key(&mut key_buf, &self.path, self.current_chunk)?;
61
62            let tx = self.db.read_transaction().await;
63            tx.read(key_buf.as_bytes(), &mut self.cache)
64                .await
65                .map_err(Error::from_db)?;
66            self.cache_valid = true;
67        }
68
69        let total_read_so_far = (self.current_chunk * CHUNK_SIZE) + self.chunk_offset;
70        let bytes_left_in_file = self.meta.size.saturating_sub(total_read_so_far);
71        let bytes_left_in_chunk = CHUNK_SIZE - self.chunk_offset;
72
73        let to_copy = core::cmp::min(buf.len(), bytes_left_in_chunk);
74        let to_copy = core::cmp::min(to_copy, bytes_left_in_file);
75
76        if to_copy == 0 {
77            return Ok(0);
78        }
79
80        buf[..to_copy].copy_from_slice(&self.cache[self.chunk_offset..self.chunk_offset + to_copy]);
81        self.chunk_offset += to_copy;
82
83        if self.chunk_offset == CHUNK_SIZE || total_read_so_far + to_copy == self.meta.size {
84            self.current_chunk += 1;
85            self.chunk_offset = 0;
86            self.cache_valid = false;
87        }
88
89        Ok(to_copy)
90    }
91}