use crate::storage::{FileHeader, PAGE_SIZE, StorageBackend};
use anyhow::Result;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub struct FileBackend {
#[allow(dead_code)]
path: PathBuf,
file: File,
header: FileHeader,
is_new: bool,
}
impl FileBackend {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)?;
let file_len = file.metadata()?.len();
let is_new = file_len < PAGE_SIZE as u64;
let header = if file_len >= PAGE_SIZE as u64 {
match Self::read_header(&mut file) {
Ok(header) => header,
Err(e) => {
anyhow::bail!(
"Failed to read header from existing file (size={}): {}",
file_len,
e
);
}
}
} else {
let header = FileHeader::new();
Self::write_header(&mut file, &header)?;
header
};
Ok(FileBackend {
path,
file,
header,
is_new,
})
}
fn read_header(file: &mut File) -> Result<FileHeader> {
file.seek(SeekFrom::Start(0))?;
let mut header_bytes = vec![0u8; PAGE_SIZE];
file.read_exact(&mut header_bytes)?;
let header = FileHeader::from_bytes(&header_bytes)?;
header.validate()?;
Ok(header)
}
fn write_header(file: &mut File, header: &FileHeader) -> Result<()> {
file.seek(SeekFrom::Start(0))?;
let header_bytes = header.to_bytes();
let mut page = vec![0u8; PAGE_SIZE];
page[..header_bytes.len()].copy_from_slice(&header_bytes);
file.write_all(&page)?;
file.sync_all()?;
Ok(())
}
#[allow(dead_code)]
pub fn path(&self) -> &Path {
&self.path
}
}
impl StorageBackend for FileBackend {
fn write_page(&mut self, page_id: u64, data: &[u8]) -> Result<()> {
if data.len() != PAGE_SIZE {
anyhow::bail!(
"Invalid page size: {} bytes (expected {})",
data.len(),
PAGE_SIZE
);
}
let offset = page_id * PAGE_SIZE as u64;
self.file.seek(SeekFrom::Start(offset))?;
self.file.write_all(data)?;
if page_id == 0 {
self.header = FileHeader::from_bytes(data)?;
} else if page_id >= self.header.page_count {
self.header.page_count = page_id + 1;
Self::write_header(&mut self.file, &self.header)?;
}
Ok(())
}
fn read_page(&self, page_id: u64) -> Result<Vec<u8>> {
if page_id >= self.header.page_count {
anyhow::bail!(
"Page {} out of bounds (total pages: {})",
page_id,
self.header.page_count
);
}
let offset = page_id * PAGE_SIZE as u64;
let mut file = &self.file;
file.seek(SeekFrom::Start(offset))?;
let mut data = vec![0u8; PAGE_SIZE];
file.read_exact(&mut data)?;
Ok(data)
}
fn sync(&mut self) -> Result<()> {
self.file.sync_all()?;
Ok(())
}
fn page_count(&self) -> Result<u64> {
Ok(self.header.page_count)
}
fn close(&mut self) -> Result<()> {
self.sync()
}
fn backend_name(&self) -> &'static str {
"file"
}
fn is_new(&self) -> bool {
self.is_new
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_backend_create() {
let dir = tempfile::tempdir().unwrap();
let temp_path = dir.path().join("test_minigraf_create.graph");
let backend = FileBackend::open(&temp_path).unwrap();
assert_eq!(backend.backend_name(), "file");
assert_eq!(backend.page_count().unwrap(), 1); assert!(backend.is_new(), "newly created file should be new");
}
#[test]
fn test_file_backend_existing_file_not_new() {
let dir = tempfile::tempdir().unwrap();
let temp_path = dir.path().join("test_minigraf_existing.graph");
{
let backend = FileBackend::open(&temp_path).unwrap();
assert!(backend.is_new(), "first open should be new");
}
{
let backend = FileBackend::open(&temp_path).unwrap();
assert!(
!backend.is_new(),
"reopening existing file should not be new"
);
}
{
let backend = FileBackend::open(&temp_path).unwrap();
assert!(!backend.is_new(), "third open should still not be new");
}
}
#[test]
fn test_file_backend_write_read() {
let dir = tempfile::tempdir().unwrap();
let temp_path = dir.path().join("test_minigraf_write_read.graph");
let mut backend = FileBackend::open(&temp_path).unwrap();
let data = vec![42u8; PAGE_SIZE];
backend.write_page(1, &data).unwrap();
let read_data = backend.read_page(1).unwrap();
assert_eq!(data, read_data);
}
#[test]
fn test_file_backend_persistence() {
let dir = tempfile::tempdir().unwrap();
let temp_path = dir.path().join("test_file_backend_persistence.graph");
{
let mut backend = FileBackend::open(&temp_path).unwrap();
let data = vec![99u8; PAGE_SIZE];
backend.write_page(1, &data).unwrap();
backend.close().unwrap();
}
{
let backend = FileBackend::open(&temp_path).unwrap();
let read_data = backend.read_page(1).unwrap();
assert_eq!(read_data[0], 99);
}
}
#[test]
fn test_file_backend_page_count() {
let dir = tempfile::tempdir().unwrap();
let temp_path = dir.path().join("test_minigraf_page_count.graph");
let mut backend = FileBackend::open(&temp_path).unwrap();
assert_eq!(backend.page_count().unwrap(), 1);
backend.write_page(1, &vec![0u8; PAGE_SIZE]).unwrap();
assert_eq!(backend.page_count().unwrap(), 2);
backend.write_page(2, &vec![0u8; PAGE_SIZE]).unwrap();
assert_eq!(backend.page_count().unwrap(), 3);
}
}