ekv-fs 0.2.0

A chunked, #![no_std] Virtual File System built on top of the Embassy ekv key-value store.
Documentation
//! Streaming read handle for a single open file.

use ekv::{Database, flash::Flash};
use embassy_sync::blocking_mutex::raw::RawMutex;
use heapless::String;

use crate::error::Error;
use crate::key::{self, KEY_BUF_CAP, KEY_SUFFIX_OVERHEAD};
use crate::meta::FileMeta;

/// Handle for sequential, chunk-cached reads of one file.
pub struct EkvFile<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize> {
    db: &'a Database<F, M>,
    path: String<MAX_PATH_LEN>,
    meta: FileMeta,

    current_chunk: usize,
    chunk_offset: usize,
    cache: [u8; CHUNK_SIZE],
    cache_valid: bool,
}

impl<'a, F: Flash, M: RawMutex, const MAX_PATH_LEN: usize, const CHUNK_SIZE: usize>
    EkvFile<'a, F, M, MAX_PATH_LEN, CHUNK_SIZE>
{
    const _KEY_LEN_OK: () = assert!(MAX_PATH_LEN + KEY_SUFFIX_OVERHEAD <= KEY_BUF_CAP);

    pub(crate) fn new(db: &'a Database<F, M>, path: String<MAX_PATH_LEN>, meta: FileMeta) -> Self {
        Self {
            db,
            path,
            meta,
            current_chunk: 0,
            chunk_offset: 0,
            cache: [0; CHUNK_SIZE],
            cache_valid: false,
        }
    }

    /// File metadata (size and chunk count).
    pub const fn meta(&self) -> &FileMeta {
        &self.meta
    }

    /// Total file size in bytes.
    pub const fn size(&self) -> usize {
        self.meta.size
    }

    /// Reads up to `buf.len()` bytes, advancing an internal offset.
    ///
    /// Returns `0` at end-of-file.
    pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
        if self.current_chunk >= self.meta.chunks {
            return Ok(0);
        }

        if !self.cache_valid {
            let mut key_buf = String::<KEY_BUF_CAP>::new();
            key::format_chunk_key(&mut key_buf, &self.path, self.current_chunk)?;

            let tx = self.db.read_transaction().await;
            tx.read(key_buf.as_bytes(), &mut self.cache)
                .await
                .map_err(Error::from_db)?;
            self.cache_valid = true;
        }

        let total_read_so_far = (self.current_chunk * CHUNK_SIZE) + self.chunk_offset;
        let bytes_left_in_file = self.meta.size.saturating_sub(total_read_so_far);
        let bytes_left_in_chunk = CHUNK_SIZE - self.chunk_offset;

        let to_copy = core::cmp::min(buf.len(), bytes_left_in_chunk);
        let to_copy = core::cmp::min(to_copy, bytes_left_in_file);

        if to_copy == 0 {
            return Ok(0);
        }

        buf[..to_copy].copy_from_slice(&self.cache[self.chunk_offset..self.chunk_offset + to_copy]);
        self.chunk_offset += to_copy;

        if self.chunk_offset == CHUNK_SIZE || total_read_so_far + to_copy == self.meta.size {
            self.current_chunk += 1;
            self.chunk_offset = 0;
            self.cache_valid = false;
        }

        Ok(to_copy)
    }
}