1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::pin::Pin;
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use futures::{Stream, stream};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use crate::{
13 ByteRange, ContentQuery, CopyOptions, CreateOptions, FindQuery, FsResult, LinkTarget,
14 MoveOptions, MutationOptions, Node, NodeId, PageRequest, ReadOptions, RemoveOptions, Revision,
15 StatOptions, TouchOptions, TreeOptions, VirtualPath, WorkspaceId, WriteOptions,
16};
17
18pub type ByteStream = Pin<Box<dyn Stream<Item = FsResult<Bytes>> + Send + 'static>>;
20
21#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Capability {
25 Read,
27 Write,
29 Delete,
31 TrashRestore,
33 WorkspaceAdmin,
35}
36
37#[non_exhaustive]
39#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
40pub struct RequestContext {
41 pub workspace_id: WorkspaceId,
43 pub actor_metadata: BTreeMap<String, Value>,
45 pub capabilities: BTreeSet<Capability>,
47}
48
49impl RequestContext {
50 pub fn new(
52 workspace_id: WorkspaceId,
53 actor_metadata: BTreeMap<String, Value>,
54 capabilities: impl IntoIterator<Item = Capability>,
55 ) -> Self {
56 Self {
57 workspace_id,
58 actor_metadata,
59 capabilities: capabilities.into_iter().collect(),
60 }
61 }
62
63 pub fn trusted(workspace_id: WorkspaceId) -> Self {
65 Self::new(
66 workspace_id,
67 BTreeMap::new(),
68 [
69 Capability::Read,
70 Capability::Write,
71 Capability::Delete,
72 Capability::TrashRestore,
73 Capability::WorkspaceAdmin,
74 ],
75 )
76 }
77
78 pub fn has_capability(&self, capability: Capability) -> bool {
80 self.capabilities.contains(&capability)
81 }
82}
83
84pub struct WriteSource {
86 stream: ByteStream,
87}
88
89impl fmt::Debug for WriteSource {
90 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91 formatter
92 .debug_struct("WriteSource")
93 .finish_non_exhaustive()
94 }
95}
96
97impl WriteSource {
98 pub fn new(source: impl Stream<Item = FsResult<Bytes>> + Send + 'static) -> Self {
100 Self {
101 stream: Box::pin(source),
102 }
103 }
104
105 pub fn from_bytes(bytes: impl Into<Bytes>) -> Self {
107 let bytes = bytes.into();
108 Self::new(stream::once(async move { Ok(bytes) }))
109 }
110
111 pub fn into_stream(self) -> ByteStream {
113 self.stream
114 }
115}
116
117pub struct FileRead {
119 pub logical_length: u64,
121 pub revision: Revision,
123 pub range: ByteRange,
125 pub stream: ByteStream,
127}
128
129impl fmt::Debug for FileRead {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter
132 .debug_struct("FileRead")
133 .field("logical_length", &self.logical_length)
134 .field("revision", &self.revision)
135 .field("range", &self.range)
136 .field("stream", &"<byte stream>")
137 .finish()
138 }
139}
140
141impl FileRead {
142 pub fn into_stream(self) -> ByteStream {
144 self.stream
145 }
146}
147
148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
150pub struct WorkspaceUsage {
151 pub workspace_id: WorkspaceId,
153 pub active_logical_bytes: u64,
155 pub trashed_logical_bytes: u64,
157 pub staged_bytes: u64,
162 pub active_nodes: u64,
164 pub trashed_nodes: u64,
166 pub max_logical_bytes: u64,
168 pub max_nodes: u64,
170 pub max_file_bytes: u64,
172}
173
174#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
176pub struct TreeEntry {
177 pub path: VirtualPath,
179 pub depth: u32,
181 pub node: Node,
183}
184
185#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
187pub struct Page<T> {
188 pub items: Vec<T>,
190 pub next_cursor: Option<String>,
192}
193
194impl<T> Page<T> {
195 pub fn new(items: Vec<T>, next_cursor: Option<String>) -> Self {
197 Self { items, next_cursor }
198 }
199}
200
201#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
203#[serde(transparent)]
204pub struct TrashId(Uuid);
205
206impl TrashId {
207 pub fn new() -> Self {
209 Self(Uuid::now_v7())
210 }
211
212 pub fn parse(input: &str) -> Result<Self, uuid::Error> {
214 Uuid::parse_str(input).map(Self)
215 }
216}
217
218impl Default for TrashId {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl fmt::Display for TrashId {
225 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226 self.0.fmt(formatter)
227 }
228}
229
230#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
232pub struct TrashEntry {
233 pub id: TrashId,
235 pub node: Node,
237 pub original_path: VirtualPath,
239 pub trashed_at_ms: i64,
241 pub actor_metadata: BTreeMap<String, Value>,
243}
244
245#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
247pub struct SearchMatch {
248 pub node: Node,
250 pub path: VirtualPath,
252 pub range: ByteRange,
254 pub preview: Vec<u8>,
256}
257
258#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
260#[serde(transparent)]
261pub struct ChangeCursor(String);
262
263impl ChangeCursor {
264 pub fn new(cursor: impl Into<String>) -> Self {
266 Self(cursor.into())
267 }
268
269 pub fn as_str(&self) -> &str {
271 &self.0
272 }
273}
274
275impl fmt::Display for ChangeCursor {
276 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277 formatter.write_str(self.as_str())
278 }
279}
280
281#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
283#[serde(rename_all = "snake_case")]
284pub enum ChangeKind {
285 Created,
287 Modified,
289 Copied,
291 Moved,
293 Removed,
295 Trashed,
297 Restored,
299 Purged,
301 AttributeSet,
303 AttributeRemoved,
305}
306
307#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
309pub struct Change {
310 pub sequence: u64,
312 pub kind: ChangeKind,
314 pub node_id: Option<NodeId>,
316 pub old_path: Option<VirtualPath>,
318 pub new_path: Option<VirtualPath>,
320 pub revision: Option<Revision>,
322 pub created_at_ms: i64,
324 pub actor_metadata: BTreeMap<String, Value>,
326}
327
328#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
334#[serde(rename_all = "snake_case")]
335pub enum BatchOperation {
336 Mkdir {
338 path: VirtualPath,
340 options: CreateOptions,
342 },
343 Touch {
345 path: VirtualPath,
347 options: TouchOptions,
349 },
350 Copy {
352 from: VirtualPath,
354 to: VirtualPath,
356 options: CopyOptions,
358 },
359 Move {
361 from: VirtualPath,
363 to: VirtualPath,
365 options: MoveOptions,
367 },
368 Remove {
370 path: VirtualPath,
372 options: RemoveOptions,
374 },
375 Symlink {
377 target: LinkTarget,
379 link: VirtualPath,
381 options: CreateOptions,
383 },
384 Trash {
386 path: VirtualPath,
388 options: MutationOptions,
390 },
391 Restore {
393 trash: TrashId,
395 destination: Option<VirtualPath>,
397 options: MutationOptions,
399 },
400 Purge {
402 trash: TrashId,
404 },
405 SetAttribute {
407 path: VirtualPath,
409 key: String,
411 value: Vec<u8>,
413 options: MutationOptions,
415 },
416 RemoveAttribute {
418 path: VirtualPath,
420 key: String,
422 options: MutationOptions,
424 },
425}
426
427#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
429#[serde(rename_all = "snake_case")]
430pub enum BatchResult {
431 Node(Node),
433 Trash(TrashEntry),
435 Unit,
437}
438
439#[async_trait]
444pub trait FileSystem: Send + Sync {
445 async fn workspace_usage(&self, ctx: &RequestContext) -> FsResult<WorkspaceUsage>;
447
448 async fn stat(
450 &self,
451 ctx: &RequestContext,
452 path: &VirtualPath,
453 options: StatOptions,
454 ) -> FsResult<Node>;
455
456 async fn exists(
458 &self,
459 ctx: &RequestContext,
460 path: &VirtualPath,
461 options: StatOptions,
462 ) -> FsResult<bool>;
463
464 async fn read_dir(
466 &self,
467 ctx: &RequestContext,
468 path: &VirtualPath,
469 page: PageRequest,
470 ) -> FsResult<Page<Node>>;
471
472 async fn tree(
474 &self,
475 ctx: &RequestContext,
476 path: &VirtualPath,
477 options: TreeOptions,
478 page: PageRequest,
479 ) -> FsResult<Page<TreeEntry>>;
480
481 async fn mkdir(
483 &self,
484 ctx: &RequestContext,
485 path: &VirtualPath,
486 options: CreateOptions,
487 ) -> FsResult<Node>;
488
489 async fn read(
491 &self,
492 ctx: &RequestContext,
493 path: &VirtualPath,
494 options: ReadOptions,
495 ) -> FsResult<FileRead>;
496
497 async fn write(
499 &self,
500 ctx: &RequestContext,
501 path: &VirtualPath,
502 source: WriteSource,
503 options: WriteOptions,
504 ) -> FsResult<Node>;
505
506 async fn write_at(
508 &self,
509 ctx: &RequestContext,
510 path: &VirtualPath,
511 offset: u64,
512 source: WriteSource,
513 options: WriteOptions,
514 ) -> FsResult<Node>;
515
516 async fn append(
518 &self,
519 ctx: &RequestContext,
520 path: &VirtualPath,
521 source: WriteSource,
522 options: WriteOptions,
523 ) -> FsResult<Node>;
524
525 async fn truncate(
527 &self,
528 ctx: &RequestContext,
529 path: &VirtualPath,
530 length: u64,
531 options: MutationOptions,
532 ) -> FsResult<Node>;
533
534 async fn touch(
536 &self,
537 ctx: &RequestContext,
538 path: &VirtualPath,
539 options: TouchOptions,
540 ) -> FsResult<Node>;
541
542 async fn copy(
544 &self,
545 ctx: &RequestContext,
546 from: &VirtualPath,
547 to: &VirtualPath,
548 options: CopyOptions,
549 ) -> FsResult<Node>;
550
551 async fn move_path(
553 &self,
554 ctx: &RequestContext,
555 from: &VirtualPath,
556 to: &VirtualPath,
557 options: MoveOptions,
558 ) -> FsResult<Node>;
559
560 async fn remove(
562 &self,
563 ctx: &RequestContext,
564 path: &VirtualPath,
565 options: RemoveOptions,
566 ) -> FsResult<()>;
567
568 async fn symlink(
570 &self,
571 ctx: &RequestContext,
572 target: &LinkTarget,
573 link: &VirtualPath,
574 options: CreateOptions,
575 ) -> FsResult<Node>;
576
577 async fn read_link(&self, ctx: &RequestContext, path: &VirtualPath) -> FsResult<LinkTarget>;
579
580 async fn trash(
582 &self,
583 ctx: &RequestContext,
584 path: &VirtualPath,
585 options: MutationOptions,
586 ) -> FsResult<TrashEntry>;
587
588 async fn list_trash(
590 &self,
591 ctx: &RequestContext,
592 page: PageRequest,
593 ) -> FsResult<Page<TrashEntry>>;
594
595 async fn restore(
597 &self,
598 ctx: &RequestContext,
599 trash: TrashId,
600 destination: Option<&VirtualPath>,
601 options: MutationOptions,
602 ) -> FsResult<Node>;
603
604 async fn purge(&self, ctx: &RequestContext, trash: TrashId) -> FsResult<()>;
606
607 async fn set_attribute(
609 &self,
610 ctx: &RequestContext,
611 path: &VirtualPath,
612 key: &str,
613 value: &[u8],
614 options: MutationOptions,
615 ) -> FsResult<Node>;
616
617 async fn remove_attribute(
619 &self,
620 ctx: &RequestContext,
621 path: &VirtualPath,
622 key: &str,
623 options: MutationOptions,
624 ) -> FsResult<Node>;
625
626 async fn glob(
628 &self,
629 ctx: &RequestContext,
630 pattern: &str,
631 page: PageRequest,
632 ) -> FsResult<Page<Node>>;
633
634 async fn find(
636 &self,
637 ctx: &RequestContext,
638 query: FindQuery,
639 page: PageRequest,
640 ) -> FsResult<Page<Node>>;
641
642 async fn search_content(
644 &self,
645 ctx: &RequestContext,
646 query: ContentQuery,
647 page: PageRequest,
648 ) -> FsResult<Page<SearchMatch>>;
649
650 async fn batch(
652 &self,
653 ctx: &RequestContext,
654 operations: Vec<BatchOperation>,
655 ) -> FsResult<Vec<BatchResult>>;
656
657 async fn changes(
659 &self,
660 ctx: &RequestContext,
661 after: Option<ChangeCursor>,
662 page: PageRequest,
663 ) -> FsResult<Page<Change>>;
664}