sausage-vfs: layers (host, memory, generated, mounts), process guards, wasi:filesystem adapter
Checkpoint. More tests and the review findings follow.
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
//! Shared in-memory directories and identity-preserving open files.
|
||||
use crate::*;
|
||||
use std::{collections::BTreeMap, io::ErrorKind as E, sync::Mutex};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Node {
|
||||
Dir,
|
||||
File(Arc<Mutex<Data>>),
|
||||
}
|
||||
struct Data {
|
||||
bytes: Vec<u8>,
|
||||
accessed: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
}
|
||||
/// A shareable memory filesystem. Clones refer to the same tree.
|
||||
#[derive(Clone)]
|
||||
pub struct Memory {
|
||||
nodes: Arc<Mutex<BTreeMap<String, Node>>>,
|
||||
}
|
||||
impl Default for Memory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nodes: Arc::new(Mutex::new(BTreeMap::from([("/".into(), Node::Dir)]))),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn parent(path: &str) -> &str {
|
||||
path.rsplit_once('/')
|
||||
.map(|(p, _)| if p.is_empty() { "/" } else { p })
|
||||
.unwrap_or("/")
|
||||
}
|
||||
fn directory(nodes: &BTreeMap<String, Node>, path: &str) -> Result<()> {
|
||||
match nodes.get(path) {
|
||||
Some(Node::Dir) => Ok(()),
|
||||
Some(_) => Err(error(E::NotADirectory)),
|
||||
None => Err(error(E::NotFound)),
|
||||
}
|
||||
}
|
||||
fn metadata(node: &Node) -> Stat {
|
||||
match node {
|
||||
Node::Dir => Stat {
|
||||
directory: true,
|
||||
size: 0,
|
||||
accessed: None,
|
||||
modified: None,
|
||||
},
|
||||
Node::File(data) => {
|
||||
let d = data.lock().unwrap();
|
||||
Stat {
|
||||
directory: false,
|
||||
size: d.bytes.len() as u64,
|
||||
accessed: d.accessed,
|
||||
modified: d.modified,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Handle {
|
||||
node: Node,
|
||||
write: bool,
|
||||
}
|
||||
#[async_trait]
|
||||
impl Layer for Memory {
|
||||
async fn stat(&self, path: &str) -> Result<Stat> {
|
||||
self.nodes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(path)
|
||||
.map(metadata)
|
||||
.ok_or_else(|| error(E::NotFound))
|
||||
}
|
||||
async fn list(&self, path: &str) -> Result<Vec<Entry>> {
|
||||
let nodes = self.nodes.lock().unwrap();
|
||||
directory(&nodes, path)?;
|
||||
Ok(nodes
|
||||
.iter()
|
||||
.filter(|(p, _)| p.as_str() != path && parent(p) == path)
|
||||
.map(|(p, n)| Entry {
|
||||
name: p.rsplit('/').next().unwrap().into(),
|
||||
directory: matches!(n, Node::Dir),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
async fn open(&self, path: &str, o: Open) -> Result<Arc<dyn File>> {
|
||||
let mut nodes = self.nodes.lock().unwrap();
|
||||
if o.truncate && !o.write {
|
||||
return Err(error(E::InvalidInput));
|
||||
}
|
||||
if nodes.contains_key(path) && o.create && o.exclusive {
|
||||
return Err(error(E::AlreadyExists));
|
||||
}
|
||||
if !nodes.contains_key(path) {
|
||||
if !o.create || o.directory {
|
||||
return Err(error(E::NotFound));
|
||||
}
|
||||
directory(&nodes, parent(path))?;
|
||||
nodes.insert(
|
||||
path.into(),
|
||||
Node::File(Arc::new(Mutex::new(Data {
|
||||
bytes: vec![],
|
||||
accessed: None,
|
||||
modified: None,
|
||||
}))),
|
||||
);
|
||||
}
|
||||
let node = nodes[path].clone();
|
||||
match &node {
|
||||
Node::Dir if o.write || o.truncate => return Err(error(E::IsADirectory)),
|
||||
Node::File(_) if o.directory => return Err(error(E::NotADirectory)),
|
||||
Node::File(d) if o.truncate => d.lock().unwrap().bytes.clear(),
|
||||
_ => {}
|
||||
}
|
||||
Ok(Arc::new(Handle {
|
||||
node,
|
||||
write: o.write,
|
||||
}))
|
||||
}
|
||||
async fn mkdir(&self, path: &str) -> Result<()> {
|
||||
let mut nodes = self.nodes.lock().unwrap();
|
||||
if nodes.contains_key(path) {
|
||||
return Err(error(E::AlreadyExists));
|
||||
}
|
||||
directory(&nodes, parent(path))?;
|
||||
nodes.insert(path.into(), Node::Dir);
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_file(&self, path: &str) -> Result<()> {
|
||||
let mut nodes = self.nodes.lock().unwrap();
|
||||
match nodes.get(path) {
|
||||
None => return Err(error(E::NotFound)),
|
||||
Some(Node::Dir) => return Err(error(E::IsADirectory)),
|
||||
_ => {}
|
||||
}
|
||||
nodes.remove(path);
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_dir(&self, path: &str) -> Result<()> {
|
||||
let mut nodes = self.nodes.lock().unwrap();
|
||||
directory(&nodes, path)?;
|
||||
if path == "/" {
|
||||
return Err(error(E::ResourceBusy));
|
||||
}
|
||||
if nodes.keys().any(|p| p != path && parent(p) == path) {
|
||||
return Err(error(E::DirectoryNotEmpty));
|
||||
}
|
||||
nodes.remove(path);
|
||||
Ok(())
|
||||
}
|
||||
async fn rename(&self, from: &str, to: &str) -> Result<()> {
|
||||
let mut nodes = self.nodes.lock().unwrap();
|
||||
let node = nodes.get(from).cloned().ok_or_else(|| error(E::NotFound))?;
|
||||
directory(&nodes, parent(to))?;
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
if from == "/" || to == "/" {
|
||||
return Err(error(E::ResourceBusy));
|
||||
}
|
||||
if to.starts_with(&format!("{from}/")) {
|
||||
return Err(error(E::InvalidInput));
|
||||
}
|
||||
if let Some(target) = nodes.get(to) {
|
||||
match (&node, target) {
|
||||
(Node::Dir, Node::File(_)) => return Err(error(E::NotADirectory)),
|
||||
(Node::File(_), Node::Dir) => return Err(error(E::IsADirectory)),
|
||||
(Node::Dir, Node::Dir) if nodes.keys().any(|p| p != to && parent(p) == to) => {
|
||||
return Err(error(E::DirectoryNotEmpty));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let prefix = format!("{from}/");
|
||||
let moved: Vec<_> = nodes
|
||||
.keys()
|
||||
.filter(|p| p.as_str() == from || p.starts_with(&prefix))
|
||||
.cloned()
|
||||
.collect();
|
||||
for p in moved {
|
||||
let n = nodes.remove(&p).unwrap();
|
||||
nodes.insert(format!("{to}{}", &p[from.len()..]), n);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Handle {
|
||||
fn data(&self) -> Result<&Mutex<Data>> {
|
||||
match &self.node {
|
||||
Node::File(d) => Ok(d),
|
||||
_ => Err(error(E::IsADirectory)),
|
||||
}
|
||||
}
|
||||
fn writable(&self) -> Result<&Mutex<Data>> {
|
||||
if !self.write {
|
||||
return Err(error(E::PermissionDenied));
|
||||
}
|
||||
self.data()
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl File for Handle {
|
||||
async fn stat(&self) -> Result<Stat> {
|
||||
Ok(metadata(&self.node))
|
||||
}
|
||||
async fn read(&self, offset: u64, length: usize) -> Result<Vec<u8>> {
|
||||
let d = self.data()?.lock().unwrap();
|
||||
let start = usize::try_from(offset)
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(d.bytes.len());
|
||||
Ok(d.bytes[start..start.saturating_add(length).min(d.bytes.len())].to_vec())
|
||||
}
|
||||
async fn write(&self, offset: u64, bytes: Vec<u8>) -> Result<usize> {
|
||||
let mut d = self.writable()?.lock().unwrap();
|
||||
let start = usize::try_from(offset).map_err(|_| error(E::InvalidInput))?;
|
||||
let end = start
|
||||
.checked_add(bytes.len())
|
||||
.ok_or_else(|| error(E::InvalidInput))?;
|
||||
if !bytes.is_empty() {
|
||||
if end > d.bytes.len() {
|
||||
d.bytes.resize(end, 0);
|
||||
}
|
||||
d.bytes[start..end].copy_from_slice(&bytes);
|
||||
}
|
||||
Ok(bytes.len())
|
||||
}
|
||||
async fn append(&self, bytes: Vec<u8>) -> Result<usize> {
|
||||
self.writable()?
|
||||
.lock()
|
||||
.unwrap()
|
||||
.bytes
|
||||
.extend_from_slice(&bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
async fn set_size(&self, size: u64) -> Result<()> {
|
||||
self.writable()?.lock().unwrap().bytes.resize(
|
||||
usize::try_from(size).map_err(|_| error(E::InvalidInput))?,
|
||||
0,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn sync(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn set_times(
|
||||
&self,
|
||||
accessed: Option<SystemTime>,
|
||||
modified: Option<SystemTime>,
|
||||
) -> Result<()> {
|
||||
let mut d = self.writable()?.lock().unwrap();
|
||||
if accessed.is_some() {
|
||||
d.accessed = accessed;
|
||||
}
|
||||
if modified.is_some() {
|
||||
d.modified = modified;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user