use uuid::Uuid;
pub const ROOT_DIR_UUID: Uuid = Uuid::nil();
const DIR_NAMESPACE: &str = "DIR";
const FILE_NAMESPACE: &str = "FILE";
const TXN_NAMESPACE: &str = "_TXN";
const OPEN_NAMESPACE: &str = "_OPEN";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyKind {
DirScatter(Uuid),
RenameIntent,
ReclaimIntent,
LinkIntent,
UnrefMarker(Uuid),
InlineBlob(Uuid),
Other,
}
#[derive(Debug, Clone, Default)]
pub struct HyperDirLayout {
base: String,
}
impl HyperDirLayout {
pub fn new() -> Self {
Self { base: String::new() }
}
pub fn with_base(base: &str) -> Self {
let base = base.trim_end_matches('/');
let base = if base.is_empty() {
String::new()
} else {
format!("{base}/")
};
Self { base }
}
pub fn dir_key(&self, uuid: &Uuid) -> String {
format!("{}{DIR_NAMESPACE}/{}", self.base, uuid)
}
pub fn file_key(&self, uuid: &Uuid) -> String {
format!("{}{FILE_NAMESPACE}/{}", self.base, uuid)
}
pub fn dir_uri(&self, bucket: &str, uuid: &Uuid) -> String {
format!("s3://{}/{}", bucket, self.dir_key(uuid))
}
pub fn file_uri(&self, bucket: &str, uuid: &Uuid) -> String {
format!("s3://{}/{}", bucket, self.file_key(uuid))
}
pub fn root_dir_uri(&self, bucket: &str) -> String {
self.dir_uri(bucket, &ROOT_DIR_UUID)
}
pub fn txn_key(&self, txn_id: &str) -> String {
format!("{}{TXN_NAMESPACE}/{}.intent", self.base, txn_id)
}
pub fn reclaim_key(&self, txn_id: &str) -> String {
format!("{}{TXN_NAMESPACE}/{}.reclaim", self.base, txn_id)
}
pub fn link_key(&self, txn_id: &str) -> String {
format!("{}{TXN_NAMESPACE}/{}.link", self.base, txn_id)
}
pub fn heartbeat_key(&self, mount_id: &str) -> String {
format!("{}{TXN_NAMESPACE}/hb.{}", self.base, mount_id)
}
pub fn open_lease_key(&self, file_uuid: &Uuid, mount_id: &str) -> String {
format!("{}{OPEN_NAMESPACE}/{}/{}", self.base, file_uuid, mount_id)
}
pub fn open_lease_prefix(&self, file_uuid: &Uuid) -> String {
format!("{}{OPEN_NAMESPACE}/{}/", self.base, file_uuid)
}
pub fn open_lease_sweep_prefix(&self) -> String {
format!("{}{OPEN_NAMESPACE}/", self.base)
}
pub fn unref_key(&self, file_uuid: &Uuid, ulid: &str) -> String {
format!("{}/{}.unref", self.file_key(file_uuid), ulid)
}
pub fn blob_key(&self, file_uuid: &Uuid, ulid: &str) -> String {
format!("{}/{}.blob", self.file_key(file_uuid), ulid)
}
pub fn blob_uri(&self, bucket: &str, file_uuid: &Uuid, ulid: &str) -> String {
format!("s3://{}/{}", bucket, self.blob_key(file_uuid, ulid))
}
pub fn blob_prefix(&self, file_uuid: &Uuid) -> String {
format!("{}/", self.file_key(file_uuid))
}
pub fn txn_prefix(&self) -> String {
format!("{}{TXN_NAMESPACE}/", self.base)
}
pub fn dir_prefix(&self) -> String {
format!("{}{DIR_NAMESPACE}/", self.base)
}
pub fn file_prefix(&self) -> String {
format!("{}{FILE_NAMESPACE}/", self.base)
}
pub fn classify_key(&self, key: &str) -> Option<KeyKind> {
let rest = key.strip_prefix(&self.base)?;
if let Some(txn) = rest.strip_prefix(&format!("{TXN_NAMESPACE}/")) {
if txn.ends_with(".intent") { return Some(KeyKind::RenameIntent); }
if txn.ends_with(".reclaim") { return Some(KeyKind::ReclaimIntent); }
if txn.ends_with(".link") { return Some(KeyKind::LinkIntent); }
return Some(KeyKind::Other);
}
if let Some(d) = rest.strip_prefix(&format!("{DIR_NAMESPACE}/")) {
if let Some((uuid_s, tail)) = d.split_once('/') {
if tail.starts_with("!/") && (tail.ends_with(".inode") || tail.ends_with(".tombstone")) {
if let Ok(u) = Uuid::parse_str(uuid_s) { return Some(KeyKind::DirScatter(u)); }
}
}
return Some(KeyKind::Other);
}
if let Some(f) = rest.strip_prefix(&format!("{FILE_NAMESPACE}/")) {
if let Some((uuid_s, tail)) = f.split_once('/') {
if tail.ends_with(".unref") {
if let Ok(u) = Uuid::parse_str(uuid_s) { return Some(KeyKind::UnrefMarker(u)); }
}
if tail.ends_with(".blob") {
if let Ok(u) = Uuid::parse_str(uuid_s) { return Some(KeyKind::InlineBlob(u)); }
}
}
return Some(KeyKind::Other);
}
Some(KeyKind::Other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_without_base() {
let l = HyperDirLayout::new();
let u = Uuid::nil();
assert_eq!(l.dir_key(&u), "DIR/00000000-0000-0000-0000-000000000000");
assert_eq!(l.file_key(&u), "FILE/00000000-0000-0000-0000-000000000000");
assert_eq!(l.dir_uri("b", &u), "s3://b/DIR/00000000-0000-0000-0000-000000000000");
assert_eq!(l.root_dir_uri("b"), "s3://b/DIR/00000000-0000-0000-0000-000000000000");
}
#[test]
fn base_is_normalized() {
assert_eq!(HyperDirLayout::with_base("fs1").dir_key(&Uuid::nil()),
"fs1/DIR/00000000-0000-0000-0000-000000000000");
assert_eq!(HyperDirLayout::with_base("fs1/").dir_key(&Uuid::nil()),
"fs1/DIR/00000000-0000-0000-0000-000000000000");
assert_eq!(HyperDirLayout::with_base("").dir_key(&Uuid::nil()),
"DIR/00000000-0000-0000-0000-000000000000");
}
#[test]
fn classify_key_cases() {
let u = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
for l in [HyperDirLayout::new(), HyperDirLayout::with_base("fs1")] {
let p = &l.base; assert_eq!(l.classify_key(&format!("{p}DIR/{u}/!/QQ/01_{u}_1.inode")), Some(KeyKind::DirScatter(u)));
assert_eq!(l.classify_key(&format!("{p}DIR/{u}/!/QQ/01_{u}_4.tombstone")), Some(KeyKind::DirScatter(u)));
assert_eq!(l.classify_key(&format!("{p}DIR/{u}/inode")), Some(KeyKind::Other));
assert_eq!(l.classify_key(&format!("{p}DIR/{u}/_compact.lease")), Some(KeyKind::Other));
assert_eq!(l.classify_key(&format!("{p}_TXN/01ABC.intent")), Some(KeyKind::RenameIntent));
assert_eq!(l.classify_key(&format!("{p}_TXN/01ABC.reclaim")), Some(KeyKind::ReclaimIntent));
assert_eq!(l.classify_key(&format!("{p}_TXN/01ABC.link")), Some(KeyKind::LinkIntent));
assert_eq!(l.classify_key(&format!("{p}FILE/{u}/01ABC.unref")), Some(KeyKind::UnrefMarker(u)));
assert_eq!(l.classify_key(&format!("{p}FILE/{u}/01ABC.blob")), Some(KeyKind::InlineBlob(u)));
assert_eq!(l.classify_key(&format!("{p}FILE/{u}/inode")), Some(KeyKind::Other));
}
assert_eq!(HyperDirLayout::with_base("fs1").classify_key("fs2/_TXN/x.intent"), None);
assert_eq!(HyperDirLayout::with_base("fs1").classify_key(&format!("DIR/{u}/inode")), None);
}
}