use std::path::PathBuf;
use std::sync::Arc;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use futures::future::BoxFuture;
use super::{Backend, CloudBackend, LocalBackend};
use crate::sandbox::fs::{FsEntry, FsMetadata};
use crate::volume::{
Volume, VolumeConfig, VolumeFsReadStream, VolumeFsWriteSink, VolumeHandle, VolumeKind,
};
use crate::{MicrosandboxError, MicrosandboxResult};
pub enum VolumeInner {
Local(VolumeLocalState),
Cloud(VolumeCloudState),
}
pub struct VolumeLocalState {
pub path: PathBuf,
pub kind: VolumeKind,
pub capacity_bytes: Option<u64>,
pub disk_format: Option<String>,
pub disk_fstype: Option<String>,
}
pub struct VolumeCloudState {
pub id: String,
pub org_id: String,
pub kind: VolumeKind,
pub capacity_bytes: Option<u64>,
pub disk_format: Option<String>,
pub disk_fstype: Option<String>,
}
pub enum VolumeHandleInner {
Local(VolumeHandleLocalState),
Cloud(VolumeHandleCloudState),
}
pub struct VolumeHandleLocalState {
pub db_id: i32,
pub path: PathBuf,
pub quota_mib: Option<u32>,
pub kind: VolumeKind,
pub used_bytes: u64,
pub capacity_bytes: Option<u64>,
pub disk_format: Option<String>,
pub disk_fstype: Option<String>,
pub labels: Vec<(String, String)>,
pub created_at: Option<DateTime<Utc>>,
}
pub struct VolumeHandleCloudState {
pub id: String,
pub org_id: String,
pub quota_mib: Option<u32>,
pub kind: VolumeKind,
pub used_bytes: u64,
pub capacity_bytes: Option<u64>,
pub disk_format: Option<String>,
pub disk_fstype: Option<String>,
pub labels: Vec<(String, String)>,
pub created_at: Option<DateTime<Utc>>,
}
pub trait VolumeBackend: Send + Sync {
fn create<'a>(
&'a self,
backend: Arc<dyn Backend>,
config: VolumeConfig,
) -> BoxFuture<'a, MicrosandboxResult<Volume>>;
fn get<'a>(
&'a self,
backend: Arc<dyn Backend>,
name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeHandle>>;
fn list<'a>(
&'a self,
backend: Arc<dyn Backend>,
) -> BoxFuture<'a, MicrosandboxResult<Vec<VolumeHandle>>>;
fn remove<'a>(
&'a self,
backend: Arc<dyn Backend>,
name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_read<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Bytes>>;
fn fs_read_to_string<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<String>>;
fn fs_write<'a>(
&'a self,
name: &'a str,
path: &'a str,
data: Vec<u8>,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_list<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Vec<FsEntry>>>;
fn fs_stat<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<FsMetadata>>;
fn fs_mkdir<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_remove<'a>(
&'a self,
name: &'a str,
path: &'a str,
recursive: bool,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_copy<'a>(
&'a self,
name: &'a str,
from: &'a str,
to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_rename<'a>(
&'a self,
name: &'a str,
from: &'a str,
to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>>;
fn fs_exists<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<bool>>;
fn fs_read_stream<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsReadStream>>;
fn fs_write_stream<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsWriteSink>>;
}
impl VolumeBackend for LocalBackend {
fn create<'a>(
&'a self,
backend: Arc<dyn Backend>,
config: VolumeConfig,
) -> BoxFuture<'a, MicrosandboxResult<Volume>> {
Box::pin(async move { crate::volume::create_local(backend, config).await })
}
fn get<'a>(
&'a self,
backend: Arc<dyn Backend>,
name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeHandle>> {
Box::pin(async move { crate::volume::get_local(backend, name).await })
}
fn list<'a>(
&'a self,
backend: Arc<dyn Backend>,
) -> BoxFuture<'a, MicrosandboxResult<Vec<VolumeHandle>>> {
Box::pin(async move { crate::volume::list_local(backend).await })
}
fn remove<'a>(
&'a self,
backend: Arc<dyn Backend>,
name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::remove_local(backend, name).await })
}
fn fs_read<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Bytes>> {
Box::pin(async move { crate::volume::fs::local::read(self, name, path).await })
}
fn fs_read_to_string<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<String>> {
Box::pin(async move { crate::volume::fs::local::read_to_string(self, name, path).await })
}
fn fs_write<'a>(
&'a self,
name: &'a str,
path: &'a str,
data: Vec<u8>,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::fs::local::write(self, name, path, &data).await })
}
fn fs_list<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Vec<FsEntry>>> {
Box::pin(async move { crate::volume::fs::local::list(self, name, path).await })
}
fn fs_stat<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<FsMetadata>> {
Box::pin(async move { crate::volume::fs::local::stat(self, name, path).await })
}
fn fs_mkdir<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::fs::local::mkdir(self, name, path).await })
}
fn fs_remove<'a>(
&'a self,
name: &'a str,
path: &'a str,
recursive: bool,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::fs::local::remove(self, name, path, recursive).await })
}
fn fs_copy<'a>(
&'a self,
name: &'a str,
from: &'a str,
to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::fs::local::copy(self, name, from, to).await })
}
fn fs_rename<'a>(
&'a self,
name: &'a str,
from: &'a str,
to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { crate::volume::fs::local::rename(self, name, from, to).await })
}
fn fs_exists<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<bool>> {
Box::pin(async move { crate::volume::fs::local::exists(self, name, path).await })
}
fn fs_read_stream<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsReadStream>> {
Box::pin(async move { crate::volume::fs::local::read_stream(self, name, path).await })
}
fn fs_write_stream<'a>(
&'a self,
name: &'a str,
path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsWriteSink>> {
Box::pin(async move { crate::volume::fs::local::write_stream(self, name, path).await })
}
}
impl VolumeBackend for CloudBackend {
fn create<'a>(
&'a self,
_backend: Arc<dyn Backend>,
_config: VolumeConfig,
) -> BoxFuture<'a, MicrosandboxResult<Volume>> {
Box::pin(async move { Err(unsupported("Volume::create")) })
}
fn get<'a>(
&'a self,
_backend: Arc<dyn Backend>,
_name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeHandle>> {
Box::pin(async move { Err(unsupported("Volume::get")) })
}
fn list<'a>(
&'a self,
_backend: Arc<dyn Backend>,
) -> BoxFuture<'a, MicrosandboxResult<Vec<VolumeHandle>>> {
Box::pin(async move { Err(unsupported("Volume::list")) })
}
fn remove<'a>(
&'a self,
_backend: Arc<dyn Backend>,
_name: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("Volume::remove")) })
}
fn fs_read<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Bytes>> {
Box::pin(async move { Err(unsupported("VolumeFs::read")) })
}
fn fs_read_to_string<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<String>> {
Box::pin(async move { Err(unsupported("VolumeFs::read_to_string")) })
}
fn fs_write<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
_data: Vec<u8>,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("VolumeFs::write")) })
}
fn fs_list<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<Vec<FsEntry>>> {
Box::pin(async move { Err(unsupported("VolumeFs::list")) })
}
fn fs_stat<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<FsMetadata>> {
Box::pin(async move { Err(unsupported("VolumeFs::stat")) })
}
fn fs_mkdir<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("VolumeFs::mkdir")) })
}
fn fs_remove<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
_recursive: bool,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("VolumeFs::remove")) })
}
fn fs_copy<'a>(
&'a self,
_name: &'a str,
_from: &'a str,
_to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("VolumeFs::copy")) })
}
fn fs_rename<'a>(
&'a self,
_name: &'a str,
_from: &'a str,
_to: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<()>> {
Box::pin(async move { Err(unsupported("VolumeFs::rename")) })
}
fn fs_exists<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<bool>> {
Box::pin(async move { Err(unsupported("VolumeFs::exists")) })
}
fn fs_read_stream<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsReadStream>> {
Box::pin(async move { Err(unsupported("VolumeFs::read_stream")) })
}
fn fs_write_stream<'a>(
&'a self,
_name: &'a str,
_path: &'a str,
) -> BoxFuture<'a, MicrosandboxResult<VolumeFsWriteSink>> {
Box::pin(async move { Err(unsupported("VolumeFs::write_stream")) })
}
}
fn unsupported(feature: &str) -> MicrosandboxError {
MicrosandboxError::Unsupported {
feature: feature.into(),
available_when: "when cloud volumes ship".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::{LocalBackend, set_default_backend};
use crate::volume::VolumeConfig;
#[tokio::test]
async fn local_backend_remove_uses_passed_backend_not_global_default() {
let home_a = tempfile::tempdir().unwrap();
let home_b = tempfile::tempdir().unwrap();
let backend_a: Arc<dyn Backend> = Arc::new(
LocalBackend::builder()
.home(home_a.path())
.build()
.await
.unwrap(),
);
let backend_b: Arc<dyn Backend> = Arc::new(
LocalBackend::builder()
.home(home_b.path())
.build()
.await
.unwrap(),
);
backend_a
.volumes()
.create(
backend_a.clone(),
VolumeConfig {
name: "shared-name".into(),
kind: crate::volume::VolumeKind::Directory,
quota_mib: None,
capacity_mib: None,
labels: Vec::new(),
},
)
.await
.unwrap();
set_default_backend(backend_a.clone());
let err = backend_b
.volumes()
.remove(backend_b.clone(), "shared-name")
.await
.expect_err("remove should fail: volume does not exist in backend B");
assert!(
matches!(err, MicrosandboxError::VolumeNotFound(_)),
"expected VolumeNotFound, got: {err:?}"
);
let handles = backend_a.volumes().list(backend_a.clone()).await.unwrap();
assert!(
handles.iter().any(|h| h.name() == "shared-name"),
"backend A's volume should still exist after the (correctly-routed) B remove"
);
}
}