use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{IndexError, IndexResult};
pub const SPLIT_MAGIC: &[u8; 8] = b"RSPLIT01";
pub const FOOTER_TAIL_LEN: u64 = 16;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileSpan {
pub offset: u64,
pub len: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SplitMeta {
pub split_id: String,
pub stream: String,
pub doc_count: u64,
pub time_start_millis: i64,
pub time_end_millis: i64,
pub mapping: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleMeta {
pub files: BTreeMap<String, FileSpan>,
pub split: SplitMeta,
}
pub fn write_bundle(
index_dir: &Path,
out: &mut (impl Write + ?Sized),
split: SplitMeta,
) -> IndexResult<BundleMeta> {
let mut names: Vec<String> = Vec::new();
for entry in std::fs::read_dir(index_dir)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(".lock") {
continue;
}
names.push(name);
}
names.sort();
let mut files = BTreeMap::new();
let mut offset = 0u64;
let mut buf = vec![0u8; 1 << 20];
for name in names {
let path = index_dir.join(&name);
let mut file = std::fs::File::open(&path)?;
let mut written = 0u64;
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
out.write_all(&buf[..n])?;
written += n as u64;
}
files.insert(
name,
FileSpan {
offset,
len: written,
},
);
offset += written;
}
let meta = BundleMeta { files, split };
let meta_json = serde_json::to_vec(&meta)
.map_err(|e| IndexError::InvalidDocument(format!("serializing split meta: {e}")))?;
out.write_all(&meta_json)?;
out.write_all(&(meta_json.len() as u64).to_le_bytes())?;
out.write_all(SPLIT_MAGIC)?;
Ok(meta)
}
pub fn parse_footer_tail(tail: &[u8]) -> IndexResult<u64> {
if tail.len() != FOOTER_TAIL_LEN as usize {
return Err(IndexError::InvalidDocument(format!(
"footer tail must be {FOOTER_TAIL_LEN} bytes, got {}",
tail.len()
)));
}
if &tail[8..16] != SPLIT_MAGIC {
return Err(IndexError::InvalidDocument(
"not a split file (bad magic)".to_string(),
));
}
Ok(u64::from_le_bytes(tail[0..8].try_into().unwrap()))
}
pub fn parse_meta(meta_json: &[u8]) -> IndexResult<BundleMeta> {
serde_json::from_slice(meta_json)
.map_err(|e| IndexError::InvalidDocument(format!("parsing split meta: {e}")))
}