Phase 6b: snapshots as directories in claw-fuse #71
+163
-24
@@ -42,6 +42,7 @@ mod sync;
|
|||||||
mod zfs;
|
mod zfs;
|
||||||
|
|
||||||
use cluster::blob::{BlobId, BlobStore};
|
use cluster::blob::{BlobId, BlobStore};
|
||||||
|
use cluster::snapshot::SnapshotStore;
|
||||||
|
|
||||||
const TTL: Duration = Duration::from_secs(1);
|
const TTL: Duration = Duration::from_secs(1);
|
||||||
// Inode number layout:
|
// Inode number layout:
|
||||||
@@ -52,34 +53,78 @@ const TTL: Duration = Duration::from_secs(1);
|
|||||||
// the whole store at mount time.
|
// the whole store at mount time.
|
||||||
const ROOT_INO: u64 = 1;
|
const ROOT_INO: u64 = 1;
|
||||||
const BLOBS_DIR_INO: u64 = 2;
|
const BLOBS_DIR_INO: u64 = 2;
|
||||||
|
const SNAPSHOTS_DIR_INO: u64 = 3;
|
||||||
|
// Snapshot dir inodes: 10_000..99_999 (up to 90k snapshots).
|
||||||
|
const FIRST_SNAPSHOT_INO: u64 = 10_000;
|
||||||
|
// Blob file inodes: 100_000..
|
||||||
const FIRST_BLOB_INO: u64 = 100_000;
|
const FIRST_BLOB_INO: u64 = 100_000;
|
||||||
|
|
||||||
/// Read-only FUSE mount over a BlobStore.
|
/// Read-only FUSE mount over a BlobStore + SnapshotStore.
|
||||||
struct ClawFuse {
|
struct ClawFuse {
|
||||||
store: BlobStore,
|
store: BlobStore,
|
||||||
/// Runtime for async BlobStore calls. fuser is sync so we
|
snapshots: SnapshotStore,
|
||||||
|
/// Runtime for async store calls. fuser is sync so we
|
||||||
/// block_on inside each callback.
|
/// block_on inside each callback.
|
||||||
runtime: tokio::runtime::Runtime,
|
runtime: tokio::runtime::Runtime,
|
||||||
/// blob-hex → allocated inode. Populated on lookup.
|
/// blob-hex → allocated inode. Populated on lookup.
|
||||||
hex_to_ino: HashMap<String, u64>,
|
hex_to_ino: HashMap<String, u64>,
|
||||||
/// inode → blob-hex. Reverse lookup for getattr / read.
|
/// inode → blob-hex. Reverse lookup for getattr / read.
|
||||||
ino_to_hex: HashMap<u64, String>,
|
ino_to_hex: HashMap<u64, String>,
|
||||||
next_ino: u64,
|
/// snapshot name → allocated inode.
|
||||||
|
snapshot_to_ino: HashMap<String, u64>,
|
||||||
|
/// inode → snapshot name.
|
||||||
|
ino_to_snapshot: HashMap<u64, String>,
|
||||||
|
next_blob_ino: u64,
|
||||||
|
next_snapshot_ino: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClawFuse {
|
impl ClawFuse {
|
||||||
fn new(store: BlobStore) -> Result<Self> {
|
fn new(store: BlobStore, snapshots: SnapshotStore) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
store,
|
store,
|
||||||
|
snapshots,
|
||||||
runtime: tokio::runtime::Builder::new_current_thread()
|
runtime: tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?,
|
.build()?,
|
||||||
hex_to_ino: HashMap::new(),
|
hex_to_ino: HashMap::new(),
|
||||||
ino_to_hex: HashMap::new(),
|
ino_to_hex: HashMap::new(),
|
||||||
next_ino: FIRST_BLOB_INO,
|
snapshot_to_ino: HashMap::new(),
|
||||||
|
ino_to_snapshot: HashMap::new(),
|
||||||
|
next_blob_ino: FIRST_BLOB_INO,
|
||||||
|
next_snapshot_ino: FIRST_SNAPSHOT_INO,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn alloc_blob_ino(&mut self, hex: &str) -> u64 {
|
||||||
|
let ino = *self
|
||||||
|
.hex_to_ino
|
||||||
|
.entry(hex.to_string())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let n = self.next_blob_ino;
|
||||||
|
self.next_blob_ino += 1;
|
||||||
|
n
|
||||||
|
});
|
||||||
|
self.ino_to_hex
|
||||||
|
.entry(ino)
|
||||||
|
.or_insert_with(|| hex.to_string());
|
||||||
|
ino
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alloc_snapshot_ino(&mut self, name: &str) -> u64 {
|
||||||
|
let ino = *self
|
||||||
|
.snapshot_to_ino
|
||||||
|
.entry(name.to_string())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let n = self.next_snapshot_ino;
|
||||||
|
self.next_snapshot_ino += 1;
|
||||||
|
n
|
||||||
|
});
|
||||||
|
self.ino_to_snapshot
|
||||||
|
.entry(ino)
|
||||||
|
.or_insert_with(|| name.to_string());
|
||||||
|
ino
|
||||||
|
}
|
||||||
|
|
||||||
fn dir_attr(ino: u64) -> FileAttr {
|
fn dir_attr(ino: u64) -> FileAttr {
|
||||||
FileAttr {
|
FileAttr {
|
||||||
ino,
|
ino,
|
||||||
@@ -133,12 +178,7 @@ impl ClawFuse {
|
|||||||
.block_on(self.store.load_manifest(&blob_id))
|
.block_on(self.store.load_manifest(&blob_id))
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
let ino = *self.hex_to_ino.entry(hex.to_string()).or_insert_with(|| {
|
let ino = self.alloc_blob_ino(hex);
|
||||||
let n = self.next_ino;
|
|
||||||
self.next_ino += 1;
|
|
||||||
n
|
|
||||||
});
|
|
||||||
self.ino_to_hex.entry(ino).or_insert_with(|| hex.to_string());
|
|
||||||
Some((ino, manifest.total_size))
|
Some((ino, manifest.total_size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,17 +196,80 @@ impl Filesystem for ClawFuse {
|
|||||||
ROOT_INO if name == "blobs" => {
|
ROOT_INO if name == "blobs" => {
|
||||||
reply.entry(&TTL, &Self::dir_attr(BLOBS_DIR_INO), 0);
|
reply.entry(&TTL, &Self::dir_attr(BLOBS_DIR_INO), 0);
|
||||||
}
|
}
|
||||||
|
ROOT_INO if name == "snapshots" => {
|
||||||
|
reply.entry(&TTL, &Self::dir_attr(SNAPSHOTS_DIR_INO), 0);
|
||||||
|
}
|
||||||
BLOBS_DIR_INO => match self.resolve_hex(name) {
|
BLOBS_DIR_INO => match self.resolve_hex(name) {
|
||||||
Some((ino, size)) => reply.entry(&TTL, &Self::file_attr(ino, size), 0),
|
Some((ino, size)) => reply.entry(&TTL, &Self::file_attr(ino, size), 0),
|
||||||
None => reply.error(libc::ENOENT),
|
None => reply.error(libc::ENOENT),
|
||||||
},
|
},
|
||||||
|
SNAPSHOTS_DIR_INO => {
|
||||||
|
// /snapshots/<name>/ — must be an existing snapshot.
|
||||||
|
let exists = self
|
||||||
|
.runtime
|
||||||
|
.block_on(self.snapshots.get(name))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some();
|
||||||
|
if exists {
|
||||||
|
let ino = self.alloc_snapshot_ino(name);
|
||||||
|
reply.entry(&TTL, &Self::dir_attr(ino), 0);
|
||||||
|
} else {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent_ino if (FIRST_SNAPSHOT_INO..FIRST_BLOB_INO).contains(&parent_ino) => {
|
||||||
|
// /snapshots/<name>/<blob-hex> — file iff hex is in
|
||||||
|
// the snapshot's blob_ids AND the blob exists.
|
||||||
|
let snap_name = match self.ino_to_snapshot.get(&parent_ino).cloned() {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manifest = match self
|
||||||
|
.runtime
|
||||||
|
.block_on(self.snapshots.get(&snap_name))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let target = match BlobId::from_hex(name) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !manifest.blob_ids.iter().any(|b| b == &target) {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match self.resolve_hex(name) {
|
||||||
|
Some((ino, size)) => {
|
||||||
|
reply.entry(&TTL, &Self::file_attr(ino, size), 0)
|
||||||
|
}
|
||||||
|
None => reply.error(libc::ENOENT),
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => reply.error(libc::ENOENT),
|
_ => reply.error(libc::ENOENT),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
|
fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
|
||||||
match ino {
|
match ino {
|
||||||
ROOT_INO | BLOBS_DIR_INO => reply.attr(&TTL, &Self::dir_attr(ino)),
|
ROOT_INO | BLOBS_DIR_INO | SNAPSHOTS_DIR_INO => {
|
||||||
|
reply.attr(&TTL, &Self::dir_attr(ino))
|
||||||
|
}
|
||||||
|
n if (FIRST_SNAPSHOT_INO..FIRST_BLOB_INO).contains(&n) => {
|
||||||
|
reply.attr(&TTL, &Self::dir_attr(ino));
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let hex = match self.ino_to_hex.get(&ino).cloned() {
|
let hex = match self.ino_to_hex.get(&ino).cloned() {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
@@ -208,6 +311,7 @@ impl Filesystem for ClawFuse {
|
|||||||
entries.push((ROOT_INO, FileType::Directory, ".".into()));
|
entries.push((ROOT_INO, FileType::Directory, ".".into()));
|
||||||
entries.push((ROOT_INO, FileType::Directory, "..".into()));
|
entries.push((ROOT_INO, FileType::Directory, "..".into()));
|
||||||
entries.push((BLOBS_DIR_INO, FileType::Directory, "blobs".into()));
|
entries.push((BLOBS_DIR_INO, FileType::Directory, "blobs".into()));
|
||||||
|
entries.push((SNAPSHOTS_DIR_INO, FileType::Directory, "snapshots".into()));
|
||||||
}
|
}
|
||||||
BLOBS_DIR_INO => {
|
BLOBS_DIR_INO => {
|
||||||
entries.push((BLOBS_DIR_INO, FileType::Directory, ".".into()));
|
entries.push((BLOBS_DIR_INO, FileType::Directory, ".".into()));
|
||||||
@@ -221,17 +325,50 @@ impl Filesystem for ClawFuse {
|
|||||||
};
|
};
|
||||||
for id in ids {
|
for id in ids {
|
||||||
let hex = id.to_hex();
|
let hex = id.to_hex();
|
||||||
let ino = *self
|
let ino = self.alloc_blob_ino(&hex);
|
||||||
.hex_to_ino
|
entries.push((ino, FileType::RegularFile, hex));
|
||||||
.entry(hex.clone())
|
}
|
||||||
.or_insert_with(|| {
|
}
|
||||||
let n = self.next_ino;
|
SNAPSHOTS_DIR_INO => {
|
||||||
self.next_ino += 1;
|
entries.push((SNAPSHOTS_DIR_INO, FileType::Directory, ".".into()));
|
||||||
n
|
entries.push((ROOT_INO, FileType::Directory, "..".into()));
|
||||||
});
|
let summaries = match self.runtime.block_on(self.snapshots.list()) {
|
||||||
self.ino_to_hex
|
Ok(v) => v,
|
||||||
.entry(ino)
|
Err(_) => {
|
||||||
.or_insert_with(|| hex.clone());
|
reply.error(libc::EIO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for s in summaries {
|
||||||
|
let ino = self.alloc_snapshot_ino(&s.name);
|
||||||
|
entries.push((ino, FileType::Directory, s.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snap_ino if (FIRST_SNAPSHOT_INO..FIRST_BLOB_INO).contains(&snap_ino) => {
|
||||||
|
entries.push((snap_ino, FileType::Directory, ".".into()));
|
||||||
|
entries.push((SNAPSHOTS_DIR_INO, FileType::Directory, "..".into()));
|
||||||
|
let name = match self.ino_to_snapshot.get(&snap_ino).cloned() {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let manifest = match self
|
||||||
|
.runtime
|
||||||
|
.block_on(self.snapshots.get(&name))
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
reply.error(libc::ENOENT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for id in manifest.blob_ids {
|
||||||
|
let hex = id.to_hex();
|
||||||
|
let ino = self.alloc_blob_ino(&hex);
|
||||||
entries.push((ino, FileType::RegularFile, hex));
|
entries.push((ino, FileType::RegularFile, hex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +449,9 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
let store = BlobStore::open(cli.data_dir.clone())
|
let store = BlobStore::open(cli.data_dir.clone())
|
||||||
.with_context(|| format!("opening blob store at {}", cli.data_dir.display()))?;
|
.with_context(|| format!("opening blob store at {}", cli.data_dir.display()))?;
|
||||||
let fs = ClawFuse::new(store)?;
|
let snapshots = SnapshotStore::open(cli.data_dir.clone())
|
||||||
|
.with_context(|| format!("opening snapshot store at {}", cli.data_dir.display()))?;
|
||||||
|
let fs = ClawFuse::new(store, snapshots)?;
|
||||||
|
|
||||||
let mut opts = vec![
|
let mut opts = vec![
|
||||||
MountOption::RO,
|
MountOption::RO,
|
||||||
|
|||||||
Reference in New Issue
Block a user