use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const DEFAULT_UA: &str = "arcform-http_fetch/1 (+https://meridian.online)";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FetchMeta {
pub url: String,
#[serde(default)]
pub request_headers: BTreeMap<String, String>,
#[serde(default)]
pub etag: Option<String>,
#[serde(default)]
pub last_modified: Option<String>,
#[serde(default)]
pub sha256: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_sha256: Option<String>,
#[serde(default)]
pub fetched_unix: Option<u64>,
}
pub fn meta_path(out: &Path) -> PathBuf {
let mut s = out.as_os_str().to_owned();
s.push(".arcmeta");
PathBuf::from(s)
}
pub fn read(out: &Path) -> Option<FetchMeta> {
let s = std::fs::read_to_string(meta_path(out)).ok()?;
serde_yaml::from_str(&s).ok()
}
pub fn write(out: &Path, meta: &FetchMeta) -> std::io::Result<()> {
let s = serde_yaml::to_string(meta).map_err(std::io::Error::other)?;
std::fs::write(meta_path(out), s)
}
pub fn remove(out: &Path) -> std::io::Result<()> {
match std::fs::remove_file(meta_path(out)) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_path_appends_not_replaces() {
assert_eq!(
meta_path(Path::new("build/edgar.parquet")),
PathBuf::from("build/edgar.parquet.arcmeta")
);
}
#[test]
fn sidecar_round_trips() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("edgar.parquet");
let mut headers = BTreeMap::new();
headers.insert("User-Agent".to_string(), "meridian".to_string());
let meta = FetchMeta {
url: "https://openlake.meridian.online/edgar.parquet".to_string(),
request_headers: headers,
etag: Some("\"abc123\"".to_string()),
last_modified: Some("Sat, 04 Jul 2026 06:31:19 GMT".to_string()),
sha256: "deadbeef".to_string(),
content_sha256: Some("c0ffee".to_string()),
fetched_unix: Some(1_784_000_000),
};
write(&out, &meta).unwrap();
let back = read(&out).expect("sidecar reads back");
assert_eq!(back.url, meta.url);
assert_eq!(back.etag, meta.etag);
assert_eq!(back.last_modified, meta.last_modified);
assert_eq!(back.sha256, meta.sha256);
assert_eq!(back.content_sha256, meta.content_sha256);
assert_eq!(
back.request_headers.get("User-Agent").map(String::as_str),
Some("meridian")
);
}
#[test]
fn absent_content_hash_is_not_serialised() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("edgar.parquet");
let meta = FetchMeta {
url: "https://openlake.meridian.online/edgar.parquet".to_string(),
etag: Some("\"abc123\"".to_string()),
sha256: "deadbeef".to_string(),
..Default::default()
};
write(&out, &meta).unwrap();
let on_disk = std::fs::read_to_string(meta_path(&out)).unwrap();
assert!(!on_disk.contains("content_sha256"), "{}", on_disk);
assert!(read(&out).unwrap().content_sha256.is_none());
}
#[test]
fn read_absent_sidecar_is_none() {
let dir = tempfile::tempdir().unwrap();
assert!(read(&dir.path().join("nope.parquet")).is_none());
}
#[test]
fn remove_deletes_the_sidecar_and_leaves_the_artifact() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("edgar.parquet");
std::fs::write(&out, b"bytes").unwrap();
write(&out, &FetchMeta::default()).unwrap();
remove(&out).unwrap();
assert!(!meta_path(&out).exists(), "sidecar gone");
assert!(out.is_file(), "artifact kept");
assert!(read(&out).is_none());
}
#[test]
fn remove_absent_sidecar_is_ok() {
let dir = tempfile::tempdir().unwrap();
remove(&dir.path().join("never-fetched.parquet")).unwrap();
}
}