use std::fs::File;
use std::path::{Path, PathBuf};
use rudb_catalog::FileStamp;
use rudb_common::{Error, Result};
use crate::config::Config;
use crate::database::Database;
fn directory() -> Option<PathBuf> {
let set = |name: &str| std::env::var_os(name).filter(|value| !value.is_empty());
if let Some(directory) = set("RUDB_MIRROR_DIR") {
return Some(PathBuf::from(directory));
}
if let Some(cache) = set("XDG_CACHE_HOME") {
return Some(PathBuf::from(cache).join("rudb").join("mirror"));
}
set("HOME").map(|home| PathBuf::from(home).join(".cache").join("rudb").join("mirror"))
}
fn key(path: &str, binary_as_string: bool, stamp: FileStamp) -> Result<u128> {
let file = File::open(path)
.map_err(|error| Error::io(format!("could not open {path} to mirror it: {error}")))?;
let mut tail = [0; 8];
if stamp.size < 12 {
return Err(Error::io(format!("{path} is too short to be a Parquet file")));
}
read_at(&file, stamp.size - 8, &mut tail, path)?;
if &tail[4..] != b"PAR1" {
return Err(Error::io(format!("{path} does not end the way a Parquet file does")));
}
let length = u64::from(u32::from_le_bytes(tail[..4].try_into().expect("four bytes")));
if length + 12 > stamp.size {
return Err(Error::io(format!("{path} states a footer longer than itself")));
}
let mut material = Vec::with_capacity(path.len() + 64 + length as usize);
material.extend_from_slice(path.as_bytes());
material.push(0);
material.push(u8::from(binary_as_string));
material.extend_from_slice(&stamp.device.to_le_bytes());
material.extend_from_slice(&stamp.inode.to_le_bytes());
material.extend_from_slice(&stamp.size.to_le_bytes());
material.extend_from_slice(&stamp.modified.to_le_bytes());
let footer = material.len();
material.resize(footer + length as usize, 0);
read_at(&file, stamp.size - 8 - length, &mut material[footer..], path)?;
Ok(rudb_native::content_name(&material))
}
#[cfg(unix)]
fn read_at(file: &File, offset: u64, into: &mut [u8], path: &str) -> Result<()> {
use std::os::unix::fs::FileExt;
file.read_exact_at(into, offset)
.map_err(|error| Error::io(format!("could not read {path} to mirror it: {error}")))
}
#[cfg(not(unix))]
fn read_at(file: &File, offset: u64, into: &mut [u8], path: &str) -> Result<()> {
use std::io::{Read, Seek, SeekFrom};
let mut file = file;
file.seek(SeekFrom::Start(offset))
.and_then(|_| file.read_exact(into))
.map_err(|error| Error::io(format!("could not read {path} to mirror it: {error}")))
}
pub(crate) fn ensure(
path: &str,
binary_as_string: bool,
config: Config,
) -> Result<Option<(FileStamp, rudb_native::Reader)>> {
let Some(directory) = directory() else { return Ok(None) };
let Some(stamp) = FileStamp::of(Path::new(path)) else { return Ok(None) };
let name = key(path, binary_as_string, stamp)?;
let mirror = directory.join(format!("{name:032x}.rudb"));
if !mirror.exists() {
build(path, binary_as_string, config, &directory, &mirror)?;
if FileStamp::of(Path::new(path)) != Some(stamp) {
let _ = std::fs::remove_file(&mirror);
return Ok(None);
}
}
let native = rudb_native::Catalog::open(&mirror)?;
Ok(Some((stamp, native.table(TABLE)?)))
}
const TABLE: &str = "mirror";
fn build(
path: &str,
binary_as_string: bool,
config: Config,
directory: &Path,
mirror: &Path,
) -> Result<()> {
std::fs::create_dir_all(directory).map_err(|error| {
Error::io(format!("could not make the mirror directory {}: {error}", directory.display()))
})?;
let temporary = mirror.with_extension(format!("{}.building", std::process::id()));
let _ = std::fs::remove_file(&temporary);
let loaded = (|| {
let spelled = temporary
.to_str()
.ok_or_else(|| Error::io("the mirror directory's name is not UTF-8".to_string()))?;
let database =
Database::open_with(spelled, config.with_parquet_mirror(false).with_read_only(false))?;
let quoted = path.replace('\'', "''");
database.execute(&format!(
"CREATE TABLE {TABLE} AS SELECT * FROM read_parquet('{quoted}', \
binary_as_string={binary_as_string})"
))?;
database.execute("CHECKPOINT")?;
drop(database);
crate::database::publish(&rudb_io::RealFilesystem::new(), &temporary, mirror).map_err(
|error| {
Error::io(format!("could not publish the mirror {}: {error}", mirror.display()))
},
)
})();
if loaded.is_err() {
let _ = std::fs::remove_file(&temporary);
}
loaded
}