use super::durable::{EffectId, RunId};
use super::fs::{ContainError, ContentPath, FileIdentity};
use super::fuel::WorkBudget;
use super::vfs::{Access, Placement, VfsMode};
use super::{OpenFileGuard, QuotaCharge, QuotaExceeded, Vfs};
use std::fs::File;
use std::io::{BufWriter, Read, Seek, SeekFrom};
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ReaderOptions {
Bytes { chunk_size: usize },
Lines { maximum_bytes: u64 },
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ReadPosition {
pub offset: u64,
pub bom_handled: bool,
pub closed: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum ReadItem {
Bytes(#[serde(with = "super::effects::binary")] Vec<u8>),
Line(String),
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ReadOutput {
pub item: Option<ReadItem>,
#[serde(skip)]
pub(crate) _charge: Option<super::limits::HostBytes>,
}
impl ReadOutput {
pub fn new(item: Option<ReadItem>, reservation: super::limits::HostBytes) -> Self {
Self {
item,
_charge: Some(reservation),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum FilesystemError {
#[error(transparent)]
Containment(#[from] ContainError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Quota(#[from] QuotaExceeded),
#[error("unsupported filesystem operation: {0}")]
Unsupported(&'static str),
#[error("{0}")]
Execution(wasmtime::Error),
#[error("{0}")]
Rejected(String),
#[error(transparent)]
Exhausted(#[from] super::fuel::Exhausted),
#[error("filesystem backend failed: {0}")]
Backend(Box<dyn std::error::Error + Send + Sync>),
}
#[derive(Clone)]
pub struct MountInfo {
pub path: String,
pub volume: String,
pub access: Access,
pub maximum_bytes: Option<u64>,
}
#[derive(Clone)]
pub struct FilesystemInfo {
pub cwd: String,
pub mode: VfsMode,
pub access: Access,
pub volume: Option<String>,
pub maximum_bytes: Option<u64>,
pub mounts: Vec<MountInfo>,
}
impl FilesystemInfo {
pub fn from_vfs(vfs: &Vfs) -> Self {
Self {
cwd: vfs.cwd().into(),
mode: vfs.mode(),
access: vfs.access(),
volume: vfs.volume().map(str::to_owned),
maximum_bytes: vfs.quota().map(|q| q.limit()),
mounts: vfs
.mounts()
.iter()
.map(|m| MountInfo {
path: m.guest_path().into(),
volume: m.volume().into(),
access: m.access(),
maximum_bytes: m.quota().map(|q| q.limit()),
})
.collect(),
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct FileReader {
pub run_id: RunId,
pub id: EffectId,
pub options: ReaderOptions,
#[serde(skip)]
pub(crate) local: Option<ReaderHandle>,
}
impl FileReader {
pub fn new(run_id: RunId, id: EffectId, options: ReaderOptions) -> Self {
Self {
run_id,
id,
options,
local: None,
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct FileWriter {
pub run_id: RunId,
pub id: EffectId,
pub closed: bool,
#[serde(skip)]
pub(crate) local: Option<WriterHandle>,
}
impl FileWriter {
pub fn new(run_id: RunId, id: EffectId) -> Self {
Self {
run_id,
id,
closed: false,
local: None,
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct DirectoryReader {
pub run_id: RunId,
pub id: EffectId,
#[serde(skip)]
pub(crate) local: Option<DirectoryHandle>,
}
impl DirectoryReader {
pub fn new(run_id: RunId, id: EffectId) -> Self {
Self {
run_id,
id,
local: None,
}
}
}
pub(crate) struct ReaderHandle {
pub(crate) file: Option<File>,
pub(crate) position: ReadPosition,
pub(crate) held: Option<OpenFileGuard>,
}
pub(crate) struct DirectoryHandle {
pub(crate) entries: Option<Box<dyn Iterator<Item = DirectoryEntry> + Send + Sync>>,
}
pub(crate) struct WriterHandle {
pub(crate) open: Option<OpenWrite>,
pub(crate) temp: TempFile,
pub(crate) final_path: ContentPath,
}
pub(crate) struct TempFile {
pub(crate) path: ContentPath,
pub(crate) identity: FileIdentity,
}
pub(crate) struct OpenWrite {
pub(crate) writer: BufWriter<File>,
pub(crate) disk_charge: QuotaCharge,
pub(crate) held: Option<OpenFileGuard>,
}
#[derive(Clone, serde::Serialize)]
pub struct DirectoryEntry {
pub name: String,
pub path: String,
pub kind: &'static str,
pub bytes: u64,
}
impl<'de> serde::Deserialize<'de> for DirectoryEntry {
fn deserialize<D: serde::Deserializer<'de>>(decoder: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct Entry {
name: String,
path: String,
kind: String,
bytes: u64,
}
let entry = Entry::deserialize(decoder)?;
let kind = match entry.kind.as_str() {
"file" => "file",
"directory" => "directory",
"symlink" => "symlink",
"other" => "other",
_ => return Err(serde::de::Error::custom("invalid filesystem entry kind")),
};
Ok(Self {
name: entry.name,
path: entry.path,
kind,
bytes: entry.bytes,
})
}
}
#[async_trait::async_trait]
pub trait Filesystem: Send + Sync {
fn supports_resource_replay(&self) -> bool {
false
}
fn info(&self) -> FilesystemInfo;
fn cwd(&self) -> &str;
fn path_cost(&self, _path: &str) -> u64 {
0
}
fn placement(&self, path: &str) -> Result<Placement, FilesystemError>;
async fn metadata(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
follow: bool,
) -> Result<cap_std::fs::Metadata, FilesystemError>;
async fn read(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
offset: u64,
maximum_bytes: u64,
) -> Result<Vec<u8>, FilesystemError>;
async fn write(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
bytes: Vec<u8>,
) -> Result<(), FilesystemError>;
async fn append(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
bytes: Vec<u8>,
) -> Result<(), FilesystemError>;
async fn mkdir(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
recursive: bool,
) -> Result<(), FilesystemError>;
async fn remove(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
recursive: bool,
budget: &mut dyn MutationBudget,
) -> Result<(), FilesystemError>;
async fn move_entry(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
from: &str,
to: &str,
budget: &mut dyn MutationBudget,
) -> Result<(), FilesystemError>;
async fn copy(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
from: &str,
to: &str,
recursive: bool,
budget: &mut dyn MutationBudget,
) -> Result<(), FilesystemError>;
async fn next_reader(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
reader: &mut FileReader,
limits: &super::limits::TenantLimits,
) -> Result<ReadOutput, FilesystemError>;
async fn close_reader(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
reader: &mut FileReader,
) -> Result<(), FilesystemError>;
async fn next_directory(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
directory: &mut DirectoryReader,
limits: &super::limits::TenantLimits,
) -> Result<Option<DirectoryEntry>, FilesystemError>;
async fn close_directory(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
directory: &mut DirectoryReader,
limits: &super::limits::TenantLimits,
) -> Result<(), FilesystemError>;
async fn write_chunk(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
writer: &mut FileWriter,
bytes: Vec<u8>,
) -> Result<(), FilesystemError>;
async fn close_writer(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
writer: &mut FileWriter,
) -> Result<(), FilesystemError>;
async fn open_reader(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
options: ReaderOptions,
) -> Result<FileReader, FilesystemError>;
async fn open_directory(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
recursive: bool,
limits: &super::limits::TenantLimits,
) -> Result<DirectoryReader, FilesystemError>;
async fn open_writer(
&self,
effect_id: &EffectId,
work: Option<Arc<WorkBudget>>,
path: &str,
) -> Result<FileWriter, FilesystemError>;
}
pub fn bounded_read(
mut reader: impl Read + Seek,
offset: u64,
maximum_bytes: u64,
) -> Result<Vec<u8>, FilesystemError> {
reader.seek(SeekFrom::Start(offset))?;
let mut bytes = Vec::new();
let mut bounded = reader.take(maximum_bytes);
let mut chunk = [0; 8192];
loop {
let read = bounded.read(&mut chunk)?;
if read == 0 {
return Ok(bytes);
}
bytes
.try_reserve(read)
.map_err(|e| FilesystemError::Backend(Box::new(e)))?;
bytes.extend_from_slice(&chunk[..read]);
}
}
pub trait MutationBudget: Send {
fn begin(&mut self) -> wasmtime::Result<()>;
fn reserve(&mut self, bytes: u64) -> wasmtime::Result<()>;
fn end(&mut self);
fn effect(&mut self);
fn syscalls(&mut self, count: u64, settled: bool) -> wasmtime::Result<()>;
fn copied(&mut self, entries: u64, bytes: u64) -> wasmtime::Result<()>;
}