use std::fs;
use std::io::Read;
use std::path::Path;
use anyhow::{bail, Context};
use chrono::{DateTime, SecondsFormat, Utc};
use serde_json::{Map, Value};
use crate::format;
use crate::query::{ensure_dest_is_not_plain_file, resolve_backing};
use crate::store;
pub fn load(dir: &Path, name: &str) -> Option<Map<String, Value>> {
let text = fs::read_to_string(format::bark_path(dir, name)).ok()?;
match serde_json::from_str::<Value>(&text) {
Ok(Value::Object(map)) => Some(map),
_ => {
eprintln!(
"timberfs: warning: {} is not a JSON object; ignoring it",
format::bark_path(dir, name).display()
);
None
}
}
}
fn new_uuid() -> anyhow::Result<String> {
let mut b = [0u8; 16];
fs::File::open("/dev/urandom")?.read_exact(&mut b)?;
b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; Ok(format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13],
b[14], b[15]
))
}
pub fn with_identity(mut map: Map<String, Value>) -> anyhow::Result<Map<String, Value>> {
if !map.contains_key("id") {
map.insert("id".to_string(), Value::String(new_uuid()?));
}
if !map.contains_key("created") {
map.insert(
"created".to_string(),
Value::String(Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)),
);
}
Ok(map)
}
pub fn save(dir: &Path, name: &str, map: &Map<String, Value>) -> anyhow::Result<()> {
let map = with_identity(map.clone())?;
let text = serde_json::to_string_pretty(&Value::Object(map))?;
fs::write(format::bark_path(dir, name), text + "\n")
.with_context(|| format!("writing {}", format::bark_path(dir, name).display()))?;
Ok(())
}
pub fn index_declared(dir: &Path, name: &str) -> bool {
load(dir, name)
.and_then(|m| m.get("index").cloned())
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub fn declare_index(dir: &Path, name: &str) -> anyhow::Result<()> {
let mut map = load(dir, name).unwrap_or_default();
if map.get("index").and_then(|v| v.as_bool()) == Some(true) {
return Ok(());
}
map.insert("index".to_string(), Value::Bool(true));
save(dir, name, &map)
}
const NON_INHERITED: &[&str] = &[
"id",
"created",
"derived_from",
"derived_op",
"window_from",
"window_to",
"index",
];
pub fn ms_rfc3339(ms: u64) -> String {
DateTime::from_timestamp_millis(ms as i64)
.map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))
.unwrap_or_else(|| ms.to_string())
}
pub fn derived_map(source_bark: Option<&Map<String, Value>>, op: &str) -> Map<String, Value> {
let mut map = Map::new();
if let Some(src) = source_bark {
for (k, v) in src {
if !NON_INHERITED.contains(&k.as_str()) {
map.insert(k.clone(), v.clone());
}
}
if let Some(id) = src.get("id").and_then(|v| v.as_str()) {
map.insert("derived_from".to_string(), Value::String(id.to_string()));
}
}
map.insert("derived_op".to_string(), Value::String(op.to_string()));
map
}
pub fn ensure_identified(dir: &Path, name: &str) -> anyhow::Result<Map<String, Value>> {
let map = load(dir, name).unwrap_or_default();
if map.get("id").and_then(|v| v.as_str()).is_some() {
return Ok(map);
}
save(dir, name, &map)?; load(dir, name).context("re-reading freshly minted manifest")
}
pub fn cmd_create(dest: &Path, index: bool, sets: &[String]) -> anyhow::Result<()> {
ensure_dest_is_not_plain_file(dest, "create")?;
let (dir, name) = resolve_backing(dest)?;
fs::create_dir_all(&dir)?;
if format::rings_path(&dir, &name).exists() || format::trunk_path(&dir, &name).exists() {
bail!("{name} already exists in {}", dir.display());
}
let _dir_lock = store::lock_backing_shared(&dir)?.with_context(|| {
format!(
"backing directory {} is served by a timberfs mount",
dir.display()
)
})?;
let _file_lock = store::lock_file_exclusive(&dir, &name)?
.with_context(|| format!("{name} already has a writer"))?;
let mut map = Map::new();
if index {
map.insert("index".to_string(), Value::Bool(true));
}
for kv in sets {
let Some((k, v)) = kv.split_once('=') else {
bail!("--set wants key=value, got {kv:?}");
};
map.insert(k.trim().to_string(), Value::String(v.to_string()));
}
let mut st = store::Store {
dir: dir.clone(),
cfg: store::Config {
chunk_size: 256 * 1024,
level: 3,
flush_age_ms: 5000,
},
files: std::collections::BTreeMap::new(),
};
st.create(&name)?;
if !map.is_empty() {
save(&dir, &name, &map)?;
}
eprintln!(
"timberfs: created {}/{}{}{}",
dir.display(),
name,
if index { " (indexed)" } else { "" },
if map.is_empty() {
String::new()
} else {
format!(
" with manifest {}",
format::bark_path(&dir, &name).display()
)
}
);
Ok(())
}