use std::collections::{BTreeMap, VecDeque};
use std::fs::{self, File, OpenOptions};
use std::io::{ErrorKind, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use crate::{MessageStore, StoreError};
fn creation_time_file(path: &Path) -> Result<time::OffsetDateTime, StoreError> {
match fs::read_to_string(path) {
Ok(s) => {
let corrupted =
|| StoreError::Backend(format!("corrupted creation-time file {path:?}: {s:?}"));
let secs: i64 = s.trim().parse().map_err(|_| corrupted())?;
time::OffsetDateTime::from_unix_timestamp(secs).map_err(|_| corrupted())
}
Err(e) if e.kind() == ErrorKind::NotFound => reset_creation_time_file(path),
Err(e) => Err(io_err(e)),
}
}
fn reset_creation_time_file(path: &Path) -> Result<time::OffsetDateTime, StoreError> {
let now = time::OffsetDateTime::now_utc();
fs::write(path, now.unix_timestamp().to_string()).map_err(io_err)?;
Ok(now)
}
fn io_err(e: std::io::Error) -> StoreError {
StoreError::Io(e.to_string())
}
fn poisoned() -> StoreError {
StoreError::Backend("poisoned lock".into())
}
fn evict_oldest(index: &mut BodyIndex, max_records: usize) {
while index.len() > max_records {
let Some(&oldest) = index.keys().next() else {
break;
};
index.remove(&oldest);
}
}
#[derive(Debug, Clone, Copy)]
pub struct FileStoreOptions {
pub sync: bool,
pub max_cached_msgs: usize,
pub max_body_records: usize,
}
impl Default for FileStoreOptions {
fn default() -> Self {
Self {
sync: true,
max_cached_msgs: 0,
max_body_records: 0,
}
}
}
type BodyIndex = BTreeMap<u64, (u64, u32)>;
struct BodyLog {
path: PathBuf,
index: Mutex<BodyIndex>,
sync: bool,
max_records: usize,
}
impl BodyLog {
fn open_with_max_records(
path: PathBuf,
sync: bool,
max_records: usize,
) -> Result<(Self, bool), StoreError> {
let (mut index, corrupted, clean_len) = load_index(&path)?;
if corrupted {
let f = OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path)
.map_err(io_err)?;
f.set_len(clean_len).map_err(io_err)?;
if sync {
f.sync_data().map_err(io_err)?;
}
}
if max_records > 0 {
evict_oldest(&mut index, max_records);
}
Ok((
Self {
path,
index: Mutex::new(index),
sync,
max_records,
},
corrupted,
))
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, BodyIndex>, StoreError> {
self.index.lock().map_err(|_| poisoned())
}
fn append(&self, seq: u64, message: &[u8]) -> Result<(), StoreError> {
let mut guard = self.lock()?;
let offset = fs::metadata(&self.path).map(|m| m.len()).unwrap_or(0);
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.map_err(io_err)?;
f.write_all(&seq.to_le_bytes()).map_err(io_err)?;
let len = u32::try_from(message.len())
.map_err(|_| StoreError::Backend("message too large".into()))?;
f.write_all(&len.to_le_bytes()).map_err(io_err)?;
f.write_all(message).map_err(io_err)?;
if self.sync {
f.sync_data().map_err(io_err)?;
}
guard.insert(seq, (offset, len));
if self.max_records > 0 {
evict_oldest(&mut guard, self.max_records);
}
Ok(())
}
fn read_body_at(f: &mut File, offset: u64, len: u32) -> Result<Vec<u8>, StoreError> {
f.seek(SeekFrom::Start(offset + 12)).map_err(io_err)?;
let mut buf = vec![0u8; len as usize];
f.read_exact(&mut buf).map_err(io_err)?;
Ok(buf)
}
fn read(&self, seq: u64) -> Result<Option<Vec<u8>>, StoreError> {
let entry = self.lock()?.get(&seq).copied();
let Some((offset, len)) = entry else {
return Ok(None);
};
let mut f = File::open(&self.path).map_err(io_err)?;
Self::read_body_at(&mut f, offset, len).map(Some)
}
fn seqs_in_range(&self, begin: u64, end: u64) -> Result<Vec<u64>, StoreError> {
Ok(self.lock()?.range(begin..=end).map(|(s, _)| *s).collect())
}
fn contains(&self, seq: u64) -> Result<bool, StoreError> {
Ok(self.lock()?.contains_key(&seq))
}
fn range(&self, begin: u64, end: u64) -> Result<Vec<(u64, Vec<u8>)>, StoreError> {
let seqs = self.seqs_in_range(begin, end)?;
if seqs.is_empty() {
return Ok(Vec::new());
}
let entries: Vec<(u64, (u64, u32))> = {
let guard = self.lock()?;
seqs.into_iter()
.filter_map(|s| guard.get(&s).copied().map(|e| (s, e)))
.collect()
};
let mut f = File::open(&self.path).map_err(io_err)?;
let mut out = Vec::with_capacity(entries.len());
for (seq, (offset, len)) in entries {
out.push((seq, Self::read_body_at(&mut f, offset, len)?));
}
Ok(out)
}
fn reset(&self) -> Result<(), StoreError> {
let mut guard = self.lock()?;
let f = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&self.path)
.map_err(io_err)?;
if self.sync {
f.sync_data().map_err(io_err)?;
}
guard.clear();
Ok(())
}
}
struct SeqFile {
sender_path: PathBuf,
target_path: PathBuf,
state: Mutex<(u64, u64)>,
sync: bool,
}
impl SeqFile {
fn open(dir: &Path, sync: bool) -> Result<Self, StoreError> {
let sender_path = dir.join("senderseqnums");
let target_path = dir.join("targetseqnums");
let legacy_path = dir.join("seqnums");
if legacy_path.exists() && (!sender_path.exists() || !target_path.exists()) {
let (legacy_sender, legacy_target) = load_legacy_seqnums(&legacy_path)?;
if !sender_path.exists() {
write_seq_value(&sender_path, legacy_sender, sync)?;
}
if !target_path.exists() {
write_seq_value(&target_path, legacy_target, sync)?;
}
}
let sender = load_seq_value(&sender_path)?;
let target = load_seq_value(&target_path)?;
Ok(Self {
sender_path,
target_path,
state: Mutex::new((sender, target)),
sync,
})
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, (u64, u64)>, StoreError> {
self.state.lock().map_err(|_| poisoned())
}
fn get(&self) -> Result<(u64, u64), StoreError> {
Ok(*self.lock()?)
}
fn set_sender(&self, seq: u64) -> Result<(), StoreError> {
write_seq_value(&self.sender_path, seq, self.sync)?;
self.lock()?.0 = seq;
Ok(())
}
fn set_target(&self, seq: u64) -> Result<(), StoreError> {
write_seq_value(&self.target_path, seq, self.sync)?;
self.lock()?.1 = seq;
Ok(())
}
fn reset(&self) -> Result<(), StoreError> {
*self.lock()? = (1, 1);
let _ = fs::remove_file(&self.sender_path);
let _ = fs::remove_file(&self.target_path);
write_seq_value(&self.sender_path, 1, self.sync)?;
write_seq_value(&self.target_path, 1, self.sync)?;
Ok(())
}
}
fn write_seq_value(path: &Path, value: u64, sync: bool) -> Result<(), StoreError> {
let tmp_path = path.with_file_name(format!(
"{}.tmp",
path.file_name().and_then(|n| n.to_str()).unwrap_or("seq")
));
{
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp_path)
.map_err(io_err)?;
writeln!(f, "{value}").map_err(io_err)?;
if sync {
f.sync_data().map_err(io_err)?;
}
}
fs::rename(&tmp_path, path).map_err(io_err)?;
Ok(())
}
fn load_seq_value(path: &Path) -> Result<u64, StoreError> {
match fs::read_to_string(path) {
Ok(s) => s
.trim()
.parse()
.map_err(|_| StoreError::Backend(format!("corrupted sequence file {path:?}: {s:?}"))),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(1),
Err(e) => Err(io_err(e)),
}
}
fn load_legacy_seqnums(path: &Path) -> Result<(u64, u64), StoreError> {
let corrupt =
|s: &str| StoreError::Backend(format!("corrupted legacy seqnums file {path:?}: {s:?}"));
let s = fs::read_to_string(path).map_err(io_err)?;
let mut lines = s.lines();
let sender: u64 = lines
.next()
.ok_or_else(|| corrupt(&s))?
.trim()
.parse()
.map_err(|_| corrupt(&s))?;
let target: u64 = lines
.next()
.ok_or_else(|| corrupt(&s))?
.trim()
.parse()
.map_err(|_| corrupt(&s))?;
Ok((sender, target))
}
pub struct FileStore {
seq: SeqFile,
body: BodyLog,
corrupted: AtomicBool,
creation_time_path: PathBuf,
creation_time: Mutex<time::OffsetDateTime>,
}
impl FileStore {
pub fn open(dir: &Path) -> Result<Self, StoreError> {
Self::open_with_options(dir, FileStoreOptions::default())
}
pub fn open_with_options(dir: &Path, options: FileStoreOptions) -> Result<Self, StoreError> {
fs::create_dir_all(dir).map_err(io_err)?;
let seq = SeqFile::open(dir, options.sync)?;
let (body, corrupted) = BodyLog::open_with_max_records(
dir.join("body"),
options.sync,
options.max_body_records,
)?;
let creation_time_path = dir.join("session");
let creation_time = creation_time_file(&creation_time_path)?;
Ok(Self {
seq,
body,
corrupted: AtomicBool::new(corrupted),
creation_time_path,
creation_time: Mutex::new(creation_time),
})
}
pub fn was_corrupted(&self) -> bool {
self.corrupted.load(Ordering::SeqCst)
}
}
#[async_trait]
impl MessageStore for FileStore {
async fn next_sender_seq(&self) -> Result<u64, StoreError> {
Ok(self.seq.get()?.0)
}
async fn next_target_seq(&self) -> Result<u64, StoreError> {
Ok(self.seq.get()?.1)
}
async fn set_next_sender_seq(&self, seq: u64) -> Result<(), StoreError> {
self.seq.set_sender(seq)
}
async fn set_next_target_seq(&self, seq: u64) -> Result<(), StoreError> {
self.seq.set_target(seq)
}
async fn save(&self, seq: u64, message: &[u8]) -> Result<(), StoreError> {
self.body.append(seq, message)
}
async fn get(&self, begin: u64, end: u64) -> Result<Vec<(u64, Vec<u8>)>, StoreError> {
self.body.range(begin, end)
}
async fn reset(&self) -> Result<(), StoreError> {
self.body.reset()?;
self.seq.reset()?;
self.corrupted.store(false, Ordering::SeqCst);
let now = reset_creation_time_file(&self.creation_time_path)?;
*self.creation_time.lock().map_err(|_| poisoned())? = now;
Ok(())
}
fn was_corrupted(&self) -> bool {
self.was_corrupted()
}
async fn creation_time(&self) -> Result<Option<time::OffsetDateTime>, StoreError> {
Ok(Some(*self.creation_time.lock().map_err(|_| poisoned())?))
}
async fn save_and_advance_sender(&self, seq: u64, message: &[u8]) -> Result<(), StoreError> {
self.seq.set_sender(seq + 1)?;
self.body.append(seq, message)
}
async fn contains(&self, seq: u64) -> Result<bool, StoreError> {
self.body.contains(seq)
}
}
struct CacheState {
map: BTreeMap<u64, Vec<u8>>,
order: VecDeque<u64>,
max: usize,
}
impl CacheState {
fn new(max: usize) -> Self {
Self {
map: BTreeMap::new(),
order: VecDeque::new(),
max,
}
}
fn insert(&mut self, seq: u64, bytes: Vec<u8>) {
if self.map.insert(seq, bytes).is_none() {
self.order.push_back(seq);
}
if self.max > 0 {
while self.map.len() > self.max {
match self.order.pop_front() {
Some(oldest) => {
self.map.remove(&oldest);
}
None => break,
}
}
}
}
fn clear(&mut self) {
self.map.clear();
self.order.clear();
}
}
pub struct CachedFileStore {
seq: SeqFile,
body: BodyLog,
corrupted: AtomicBool,
cache: Mutex<CacheState>,
creation_time_path: PathBuf,
creation_time: Mutex<time::OffsetDateTime>,
}
impl CachedFileStore {
pub fn open(dir: &Path) -> Result<Self, StoreError> {
Self::open_with_options(dir, FileStoreOptions::default())
}
pub fn open_with_options(dir: &Path, options: FileStoreOptions) -> Result<Self, StoreError> {
fs::create_dir_all(dir).map_err(io_err)?;
let seq = SeqFile::open(dir, options.sync)?;
let (body, corrupted) = BodyLog::open_with_max_records(
dir.join("body"),
options.sync,
options.max_body_records,
)?;
let mut cache = CacheState::new(options.max_cached_msgs);
let all_seqs = body.seqs_in_range(0, u64::MAX)?;
let warm_from = if options.max_cached_msgs > 0 {
all_seqs.len().saturating_sub(options.max_cached_msgs)
} else {
0 };
for seq_no in all_seqs.get(warm_from..).unwrap_or_default() {
if let Some(bytes) = body.read(*seq_no)? {
cache.insert(*seq_no, bytes);
}
}
let creation_time_path = dir.join("session");
let creation_time = creation_time_file(&creation_time_path)?;
Ok(Self {
seq,
body,
corrupted: AtomicBool::new(corrupted),
cache: Mutex::new(cache),
creation_time_path,
creation_time: Mutex::new(creation_time),
})
}
pub fn was_corrupted(&self) -> bool {
self.corrupted.load(Ordering::SeqCst)
}
pub fn cached_len(&self) -> Result<usize, StoreError> {
Ok(self.cache.lock().map_err(|_| poisoned())?.map.len())
}
}
#[async_trait]
impl MessageStore for CachedFileStore {
async fn next_sender_seq(&self) -> Result<u64, StoreError> {
Ok(self.seq.get()?.0)
}
async fn next_target_seq(&self) -> Result<u64, StoreError> {
Ok(self.seq.get()?.1)
}
async fn set_next_sender_seq(&self, seq: u64) -> Result<(), StoreError> {
self.seq.set_sender(seq)
}
async fn set_next_target_seq(&self, seq: u64) -> Result<(), StoreError> {
self.seq.set_target(seq)
}
async fn save(&self, seq: u64, message: &[u8]) -> Result<(), StoreError> {
self.body.append(seq, message)?;
self.cache
.lock()
.map_err(|_| poisoned())?
.insert(seq, message.to_vec());
Ok(())
}
async fn get(&self, begin: u64, end: u64) -> Result<Vec<(u64, Vec<u8>)>, StoreError> {
let seqs = self.body.seqs_in_range(begin, end)?;
let mut out = Vec::with_capacity(seqs.len());
for seq_no in seqs {
let cached = self
.cache
.lock()
.map_err(|_| poisoned())?
.map
.get(&seq_no)
.cloned();
match cached {
Some(bytes) => out.push((seq_no, bytes)),
None => {
if let Some(bytes) = self.body.read(seq_no)? {
self.cache
.lock()
.map_err(|_| poisoned())?
.insert(seq_no, bytes.clone());
out.push((seq_no, bytes));
}
}
}
}
Ok(out)
}
async fn reset(&self) -> Result<(), StoreError> {
self.body.reset()?;
self.seq.reset()?;
self.corrupted.store(false, Ordering::SeqCst);
self.cache.lock().map_err(|_| poisoned())?.clear();
let now = reset_creation_time_file(&self.creation_time_path)?;
*self.creation_time.lock().map_err(|_| poisoned())? = now;
Ok(())
}
fn was_corrupted(&self) -> bool {
self.was_corrupted()
}
async fn creation_time(&self) -> Result<Option<time::OffsetDateTime>, StoreError> {
Ok(Some(*self.creation_time.lock().map_err(|_| poisoned())?))
}
async fn save_and_advance_sender(&self, seq: u64, message: &[u8]) -> Result<(), StoreError> {
self.seq.set_sender(seq + 1)?;
self.body.append(seq, message)?;
self.cache
.lock()
.map_err(|_| poisoned())?
.insert(seq, message.to_vec());
Ok(())
}
async fn contains(&self, seq: u64) -> Result<bool, StoreError> {
self.body.contains(seq)
}
}
fn load_index(path: &Path) -> Result<(BodyIndex, bool, u64), StoreError> {
let mut f = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok((BTreeMap::new(), false, 0)),
Err(e) => return Err(io_err(e)),
};
let file_len = f.metadata().map_err(io_err)?.len();
let mut index = BTreeMap::new();
let mut pos: u64 = 0;
loop {
if pos == file_len {
return Ok((index, false, pos));
}
if file_len - pos < 12 {
return Ok((index, true, pos)); }
let mut seq_bytes = [0u8; 8];
let mut len_bytes = [0u8; 4];
f.read_exact(&mut seq_bytes).map_err(io_err)?;
f.read_exact(&mut len_bytes).map_err(io_err)?;
let seq = u64::from_le_bytes(seq_bytes);
let len = u32::from_le_bytes(len_bytes);
let body_start = pos + 12;
let Some(body_end) = body_start.checked_add(u64::from(len)) else {
return Ok((index, true, pos));
};
if body_end > file_len {
return Ok((index, true, pos)); }
index.insert(seq, (pos, len));
f.seek(SeekFrom::Start(body_end)).map_err(io_err)?;
pos = body_end;
}
}