use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::pin::Pin;
use async_trait::async_trait;
use bytes::Bytes;
use futures::{Stream, stream};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use crate::{
ByteRange, ContentQuery, CopyOptions, CreateOptions, FindQuery, FsResult, LinkTarget,
MoveOptions, MutationOptions, Node, NodeId, PageRequest, ReadOptions, RemoveOptions, Revision,
StatOptions, TouchOptions, TreeOptions, VirtualPath, WorkspaceId, WriteOptions,
};
pub type ByteStream = Pin<Box<dyn Stream<Item = FsResult<Bytes>> + Send + 'static>>;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
Read,
Write,
Delete,
TrashRestore,
WorkspaceAdmin,
}
#[non_exhaustive]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RequestContext {
pub workspace_id: WorkspaceId,
pub actor_metadata: BTreeMap<String, Value>,
pub capabilities: BTreeSet<Capability>,
}
impl RequestContext {
pub fn new(
workspace_id: WorkspaceId,
actor_metadata: BTreeMap<String, Value>,
capabilities: impl IntoIterator<Item = Capability>,
) -> Self {
Self {
workspace_id,
actor_metadata,
capabilities: capabilities.into_iter().collect(),
}
}
pub fn trusted(workspace_id: WorkspaceId) -> Self {
Self::new(
workspace_id,
BTreeMap::new(),
[
Capability::Read,
Capability::Write,
Capability::Delete,
Capability::TrashRestore,
Capability::WorkspaceAdmin,
],
)
}
pub fn has_capability(&self, capability: Capability) -> bool {
self.capabilities.contains(&capability)
}
}
pub struct WriteSource {
stream: ByteStream,
}
impl fmt::Debug for WriteSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WriteSource")
.finish_non_exhaustive()
}
}
impl WriteSource {
pub fn new(source: impl Stream<Item = FsResult<Bytes>> + Send + 'static) -> Self {
Self {
stream: Box::pin(source),
}
}
pub fn from_bytes(bytes: impl Into<Bytes>) -> Self {
let bytes = bytes.into();
Self::new(stream::once(async move { Ok(bytes) }))
}
pub fn into_stream(self) -> ByteStream {
self.stream
}
}
pub struct FileRead {
pub logical_length: u64,
pub revision: Revision,
pub range: ByteRange,
pub stream: ByteStream,
}
impl fmt::Debug for FileRead {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("FileRead")
.field("logical_length", &self.logical_length)
.field("revision", &self.revision)
.field("range", &self.range)
.field("stream", &"<byte stream>")
.finish()
}
}
impl FileRead {
pub fn into_stream(self) -> ByteStream {
self.stream
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct WorkspaceUsage {
pub workspace_id: WorkspaceId,
pub active_logical_bytes: u64,
pub trashed_logical_bytes: u64,
pub staged_bytes: u64,
pub active_nodes: u64,
pub trashed_nodes: u64,
pub max_logical_bytes: u64,
pub max_nodes: u64,
pub max_file_bytes: u64,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TreeEntry {
pub path: VirtualPath,
pub depth: u32,
pub node: Node,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Page<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
}
impl<T> Page<T> {
pub fn new(items: Vec<T>, next_cursor: Option<String>) -> Self {
Self { items, next_cursor }
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(transparent)]
pub struct TrashId(Uuid);
impl TrashId {
pub fn new() -> Self {
Self(Uuid::now_v7())
}
pub fn parse(input: &str) -> Result<Self, uuid::Error> {
Uuid::parse_str(input).map(Self)
}
}
impl Default for TrashId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for TrashId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TrashEntry {
pub id: TrashId,
pub node: Node,
pub original_path: VirtualPath,
pub trashed_at_ms: i64,
pub actor_metadata: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SearchMatch {
pub node: Node,
pub path: VirtualPath,
pub range: ByteRange,
pub preview: Vec<u8>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(transparent)]
pub struct ChangeCursor(String);
impl ChangeCursor {
pub fn new(cursor: impl Into<String>) -> Self {
Self(cursor.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ChangeCursor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ChangeKind {
Created,
Modified,
Copied,
Moved,
Removed,
Trashed,
Restored,
Purged,
AttributeSet,
AttributeRemoved,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Change {
pub sequence: u64,
pub kind: ChangeKind,
pub node_id: Option<NodeId>,
pub old_path: Option<VirtualPath>,
pub new_path: Option<VirtualPath>,
pub revision: Option<Revision>,
pub created_at_ms: i64,
pub actor_metadata: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BatchOperation {
Mkdir {
path: VirtualPath,
options: CreateOptions,
},
Touch {
path: VirtualPath,
options: TouchOptions,
},
Copy {
from: VirtualPath,
to: VirtualPath,
options: CopyOptions,
},
Move {
from: VirtualPath,
to: VirtualPath,
options: MoveOptions,
},
Remove {
path: VirtualPath,
options: RemoveOptions,
},
Symlink {
target: LinkTarget,
link: VirtualPath,
options: CreateOptions,
},
Trash {
path: VirtualPath,
options: MutationOptions,
},
Restore {
trash: TrashId,
destination: Option<VirtualPath>,
options: MutationOptions,
},
Purge {
trash: TrashId,
},
SetAttribute {
path: VirtualPath,
key: String,
value: Vec<u8>,
options: MutationOptions,
},
RemoveAttribute {
path: VirtualPath,
key: String,
options: MutationOptions,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BatchResult {
Node(Node),
Trash(TrashEntry),
Unit,
}
#[async_trait]
pub trait FileSystem: Send + Sync {
async fn workspace_usage(&self, ctx: &RequestContext) -> FsResult<WorkspaceUsage>;
async fn stat(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: StatOptions,
) -> FsResult<Node>;
async fn exists(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: StatOptions,
) -> FsResult<bool>;
async fn read_dir(
&self,
ctx: &RequestContext,
path: &VirtualPath,
page: PageRequest,
) -> FsResult<Page<Node>>;
async fn tree(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: TreeOptions,
page: PageRequest,
) -> FsResult<Page<TreeEntry>>;
async fn mkdir(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: CreateOptions,
) -> FsResult<Node>;
async fn read(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: ReadOptions,
) -> FsResult<FileRead>;
async fn write(
&self,
ctx: &RequestContext,
path: &VirtualPath,
source: WriteSource,
options: WriteOptions,
) -> FsResult<Node>;
async fn write_at(
&self,
ctx: &RequestContext,
path: &VirtualPath,
offset: u64,
source: WriteSource,
options: WriteOptions,
) -> FsResult<Node>;
async fn append(
&self,
ctx: &RequestContext,
path: &VirtualPath,
source: WriteSource,
options: WriteOptions,
) -> FsResult<Node>;
async fn truncate(
&self,
ctx: &RequestContext,
path: &VirtualPath,
length: u64,
options: MutationOptions,
) -> FsResult<Node>;
async fn touch(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: TouchOptions,
) -> FsResult<Node>;
async fn copy(
&self,
ctx: &RequestContext,
from: &VirtualPath,
to: &VirtualPath,
options: CopyOptions,
) -> FsResult<Node>;
async fn move_path(
&self,
ctx: &RequestContext,
from: &VirtualPath,
to: &VirtualPath,
options: MoveOptions,
) -> FsResult<Node>;
async fn remove(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: RemoveOptions,
) -> FsResult<()>;
async fn symlink(
&self,
ctx: &RequestContext,
target: &LinkTarget,
link: &VirtualPath,
options: CreateOptions,
) -> FsResult<Node>;
async fn read_link(&self, ctx: &RequestContext, path: &VirtualPath) -> FsResult<LinkTarget>;
async fn trash(
&self,
ctx: &RequestContext,
path: &VirtualPath,
options: MutationOptions,
) -> FsResult<TrashEntry>;
async fn list_trash(
&self,
ctx: &RequestContext,
page: PageRequest,
) -> FsResult<Page<TrashEntry>>;
async fn restore(
&self,
ctx: &RequestContext,
trash: TrashId,
destination: Option<&VirtualPath>,
options: MutationOptions,
) -> FsResult<Node>;
async fn purge(&self, ctx: &RequestContext, trash: TrashId) -> FsResult<()>;
async fn set_attribute(
&self,
ctx: &RequestContext,
path: &VirtualPath,
key: &str,
value: &[u8],
options: MutationOptions,
) -> FsResult<Node>;
async fn remove_attribute(
&self,
ctx: &RequestContext,
path: &VirtualPath,
key: &str,
options: MutationOptions,
) -> FsResult<Node>;
async fn glob(
&self,
ctx: &RequestContext,
pattern: &str,
page: PageRequest,
) -> FsResult<Page<Node>>;
async fn find(
&self,
ctx: &RequestContext,
query: FindQuery,
page: PageRequest,
) -> FsResult<Page<Node>>;
async fn search_content(
&self,
ctx: &RequestContext,
query: ContentQuery,
page: PageRequest,
) -> FsResult<Page<SearchMatch>>;
async fn batch(
&self,
ctx: &RequestContext,
operations: Vec<BatchOperation>,
) -> FsResult<Vec<BatchResult>>;
async fn changes(
&self,
ctx: &RequestContext,
after: Option<ChangeCursor>,
page: PageRequest,
) -> FsResult<Page<Change>>;
}