use crate::backend::native::v3::constants::{DEFAULT_PAGE_SIZE, V3_HEADER_SIZE};
use crate::backend::native::{NativeBackendError, NativeResult};
use parking_lot::RwLock;
use std::fs::OpenOptions;
#[cfg(unix)]
use std::os::unix::fs::FileExt;
#[cfg(windows)]
use std::os::windows::fs::FileExt;
use std::path::Path;
pub struct FileCoordinator {
file: RwLock<CoordinatedFile>,
db_path: std::path::PathBuf,
}
struct CoordinatedFile {
file: std::fs::File,
cached_size: u64,
}
impl FileCoordinator {
pub fn create(db_path: &std::path::Path) -> NativeResult<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false) .open(db_path)
.map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to open db file for coordination: {}",
db_path.display()
),
source: e,
})?;
let cached_size = file.metadata().map(|m| m.len()).unwrap_or(0);
Ok(Self {
file: RwLock::new(CoordinatedFile { file, cached_size }),
db_path: db_path.to_path_buf(),
})
}
pub fn write_page(&self, page_id: u64, data: &[u8]) -> NativeResult<()> {
let mut coord = self.file.write();
let offset = Self::page_offset(page_id);
write_all_at(&coord.file, data, offset).map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to write page {} data ({} bytes) at offset {}",
page_id,
data.len(),
offset
),
source: e,
})?;
coord
.file
.sync_all()
.map_err(|e| NativeBackendError::IoError {
context: format!("Failed to sync page {} write", page_id),
source: e,
})?;
let actual_size = coord.file.metadata().map(|m| m.len()).unwrap_or(0);
coord.cached_size = actual_size;
Ok(())
}
pub fn read_page(&self, page_id: u64, buffer: &mut [u8]) -> NativeResult<()> {
let coord = self.file.read();
let offset = Self::page_offset(page_id);
let required_len = offset + buffer.len() as u64;
if coord.cached_size < required_len {
return Err(NativeBackendError::IoError {
context: format!(
"File too small to read page {}: cached_size={} < required_len={}",
page_id, coord.cached_size, required_len
),
source: std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"file size {} < required {}",
coord.cached_size, required_len
),
),
});
}
read_all_at(&coord.file, buffer, offset).map_err(|e| NativeBackendError::IoError {
context: format!(
"Failed to read page {} from disk at offset {}",
page_id, offset
),
source: e,
})?;
Ok(())
}
pub fn write_data_at_offset(&self, offset: u64, data: &[u8]) -> NativeResult<()> {
let mut coord = self.file.write();
let required_len = offset + data.len() as u64;
write_all_at(&coord.file, data, offset).map_err(|e| NativeBackendError::IoError {
context: format!("Failed to write external data at offset {}", offset),
source: e,
})?;
coord
.file
.sync_all()
.map_err(|e| NativeBackendError::IoError {
context: "Failed to sync external data".to_string(),
source: e,
})?;
if required_len > coord.cached_size {
coord.cached_size = required_len;
}
Ok(())
}
pub fn file_size(&self) -> u64 {
self.file.read().cached_size
}
pub fn sync_all(&self) -> NativeResult<()> {
self.file
.write()
.file
.sync_all()
.map_err(|e| NativeBackendError::IoError {
context: "Failed to sync file".to_string(),
source: e,
})
}
fn page_offset(page_id: u64) -> u64 {
if page_id == 0 {
0
} else {
V3_HEADER_SIZE + (page_id - 1) * DEFAULT_PAGE_SIZE
}
}
pub fn db_path(&self) -> &Path {
&self.db_path
}
}
fn write_all_at(file: &std::fs::File, mut data: &[u8], mut offset: u64) -> std::io::Result<()> {
while !data.is_empty() {
let written = positioned_write(file, data, offset)?;
if written == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"write_at returned 0 bytes",
));
}
data = &data[written..];
offset += written as u64;
}
Ok(())
}
fn read_all_at(file: &std::fs::File, buffer: &mut [u8], mut offset: u64) -> std::io::Result<()> {
let mut filled = 0;
while filled < buffer.len() {
let read = positioned_read(file, &mut buffer[filled..], offset)?;
if read == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"read_at returned 0 bytes before buffer was full",
));
}
filled += read;
offset += read as u64;
}
Ok(())
}
#[cfg(unix)]
fn positioned_write(file: &std::fs::File, data: &[u8], offset: u64) -> std::io::Result<usize> {
file.write_at(data, offset)
}
#[cfg(windows)]
fn positioned_write(file: &std::fs::File, data: &[u8], offset: u64) -> std::io::Result<usize> {
file.seek_write(data, offset)
}
#[cfg(unix)]
fn positioned_read(file: &std::fs::File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
file.read_at(buffer, offset)
}
#[cfg(windows)]
fn positioned_read(file: &std::fs::File, buffer: &mut [u8], offset: u64) -> std::io::Result<usize> {
file.seek_read(buffer, offset)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_coordinator_create() {
let temp = tempdir().unwrap();
let db_path = temp.path().join("test.graph");
let coordinator = FileCoordinator::create(&db_path).unwrap();
assert_eq!(coordinator.file_size(), 0);
assert_eq!(coordinator.db_path(), db_path);
}
#[test]
fn test_write_and_read_page() {
let temp = tempdir().unwrap();
let db_path = temp.path().join("test.graph");
let coordinator = FileCoordinator::create(&db_path).unwrap();
let data1 = vec![1u8; 4096];
coordinator.write_page(1, &data1).unwrap();
assert_eq!(coordinator.file_size(), V3_HEADER_SIZE + 4096);
let data2 = vec![2u8; 4096];
coordinator.write_page(2, &data2).unwrap();
assert_eq!(coordinator.file_size(), V3_HEADER_SIZE + 8192);
let mut buffer = vec![0u8; 4096];
coordinator.read_page(1, &mut buffer).unwrap();
assert_eq!(buffer, data1);
coordinator.read_page(2, &mut buffer).unwrap();
assert_eq!(buffer, data2);
}
#[test]
fn test_write_extends_file() {
let temp = tempdir().unwrap();
let db_path = temp.path().join("test.graph");
let coordinator = FileCoordinator::create(&db_path).unwrap();
let data = vec![42u8; 4096];
coordinator.write_page(100, &data).unwrap();
let expected_size = V3_HEADER_SIZE + (99 * DEFAULT_PAGE_SIZE) + 4096;
assert_eq!(coordinator.file_size(), expected_size);
let mut buffer = vec![0u8; 4096];
coordinator.read_page(100, &mut buffer).unwrap();
assert_eq!(buffer, data);
}
}