1#[path = "legacy.rs"]
8mod legacy;
9
10use self::legacy::LegacyFilesystem;
11use crate::file::{
12 CopyOptions, DeleteOptions, FileEntry, FileError, FileProvider, FileType, MkdirOptions,
13 MoveOptions, WriteOptions,
14};
15use std::collections::{BTreeMap, BTreeSet};
16use std::future::Future;
17use std::pin::Pin;
18use std::rc::Rc;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use std::time::Instant;
22
23pub type FilesystemFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, FileError>> + 'a>>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum FilesystemCapability {
27 Read,
28 Write,
29 Entries,
30 Mkdir,
31 Delete,
32 Copy,
33 Move,
34 Append,
35 AtomicMove,
36 PreserveModified,
37 RevisionCheck,
38 Transactions,
39 Watch,
40}
41
42impl FilesystemCapability {
43 pub fn keyword(self) -> &'static str {
44 match self {
45 Self::Read => "read",
46 Self::Write => "write",
47 Self::Entries => "entries",
48 Self::Mkdir => "mkdir",
49 Self::Delete => "delete",
50 Self::Copy => "copy",
51 Self::Move => "move",
52 Self::Append => "append",
53 Self::AtomicMove => "atomic-move",
54 Self::PreserveModified => "preserve-modified",
55 Self::RevisionCheck => "revision-check",
56 Self::Transactions => "transactions",
57 Self::Watch => "watch",
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Default)]
63pub struct FilesystemCapabilities {
64 values: BTreeSet<FilesystemCapability>,
65}
66
67impl FilesystemCapabilities {
68 pub fn new(values: impl IntoIterator<Item = FilesystemCapability>) -> Self {
69 Self {
70 values: values.into_iter().collect(),
71 }
72 }
73
74 pub fn read_only() -> Self {
75 Self::new([FilesystemCapability::Read, FilesystemCapability::Entries])
76 }
77
78 pub fn legacy_read_write() -> Self {
79 Self::new([
80 FilesystemCapability::Read,
81 FilesystemCapability::Write,
82 FilesystemCapability::Entries,
83 FilesystemCapability::Mkdir,
84 FilesystemCapability::Delete,
85 FilesystemCapability::Copy,
86 FilesystemCapability::Move,
87 FilesystemCapability::Append,
88 FilesystemCapability::PreserveModified,
89 ])
90 }
91
92 pub fn contains(&self, capability: FilesystemCapability) -> bool {
93 self.values.contains(&capability)
94 }
95
96 pub fn iter(&self) -> impl Iterator<Item = FilesystemCapability> + '_ {
97 self.values.iter().copied()
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FilesystemDescriptor {
103 kind: String,
104 display: String,
105 read_only: bool,
106 capabilities: FilesystemCapabilities,
107 revision: Option<String>,
108 extensions: BTreeMap<String, String>,
109}
110
111impl FilesystemDescriptor {
112 pub fn new(
113 kind: impl Into<String>,
114 display: impl Into<String>,
115 read_only: bool,
116 capabilities: FilesystemCapabilities,
117 ) -> Self {
118 Self {
119 kind: kind.into(),
120 display: display.into(),
121 read_only,
122 capabilities,
123 revision: None,
124 extensions: BTreeMap::new(),
125 }
126 }
127
128 pub fn legacy(kind: impl Into<String>, display: impl Into<String>) -> Self {
129 Self::new(
130 kind,
131 display,
132 false,
133 FilesystemCapabilities::legacy_read_write(),
134 )
135 }
136
137 pub fn with_revision(mut self, revision: impl Into<String>) -> Self {
138 self.revision = Some(revision.into());
139 self
140 }
141
142 pub fn with_extension(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
143 self.extensions.insert(key.into(), value.into());
144 self
145 }
146
147 pub fn kind(&self) -> &str {
148 &self.kind
149 }
150
151 pub fn display(&self) -> &str {
152 &self.display
153 }
154
155 pub fn read_only(&self) -> bool {
156 self.read_only
157 }
158
159 pub fn capabilities(&self) -> &FilesystemCapabilities {
160 &self.capabilities
161 }
162
163 pub fn revision(&self) -> Option<&str> {
164 self.revision.as_deref()
165 }
166
167 pub fn extensions(&self) -> &BTreeMap<String, String> {
168 &self.extensions
169 }
170}
171
172#[derive(Debug, Clone)]
173pub struct FilesystemCallContext {
174 deadline: Option<Instant>,
175 cancelled: Arc<AtomicBool>,
176 trace_id: Option<Arc<str>>,
177}
178
179impl Default for FilesystemCallContext {
180 fn default() -> Self {
181 Self {
182 deadline: None,
183 cancelled: Arc::new(AtomicBool::new(false)),
184 trace_id: None,
185 }
186 }
187}
188
189impl FilesystemCallContext {
190 pub fn with_deadline(mut self, deadline: Instant) -> Self {
191 self.deadline = Some(deadline);
192 self
193 }
194
195 pub fn with_trace_id(mut self, trace_id: impl Into<Arc<str>>) -> Self {
196 self.trace_id = Some(trace_id.into());
197 self
198 }
199
200 pub fn deadline(&self) -> Option<Instant> {
201 self.deadline
202 }
203
204 pub fn trace_id(&self) -> Option<&str> {
205 self.trace_id.as_deref()
206 }
207
208 pub fn cancel(&self) -> bool {
209 !self.cancelled.swap(true, Ordering::AcqRel)
210 }
211
212 pub fn cancelled(&self) -> bool {
213 self.cancelled.load(Ordering::Acquire)
214 }
215
216 pub fn check(&self) -> Result<(), FileError> {
217 if self.cancelled() {
218 return Err(FileError::Io("filesystem operation cancelled".into()));
219 }
220 if self
221 .deadline
222 .is_some_and(|deadline| Instant::now() >= deadline)
223 {
224 return Err(FileError::Io("filesystem operation timed out".into()));
225 }
226 Ok(())
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct FilesystemPageRequest {
232 pub token: Option<String>,
233 pub limit: usize,
234}
235
236impl Default for FilesystemPageRequest {
237 fn default() -> Self {
238 Self {
239 token: None,
240 limit: 256,
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct FilesystemEntry {
247 pub path: String,
248 pub name: String,
249 pub kind: FileType,
250 pub size: Option<u64>,
251 pub modified_at: Option<i64>,
252 pub id: Option<String>,
253 pub revision: Option<String>,
254 pub capabilities: Option<FilesystemCapabilities>,
255 pub extensions: BTreeMap<String, String>,
256}
257
258impl From<FileEntry> for FilesystemEntry {
259 fn from(entry: FileEntry) -> Self {
260 Self {
261 path: entry.path,
262 name: entry.name,
263 kind: entry.kind,
264 size: entry.size,
265 modified_at: Some(entry.modified_at),
266 id: None,
267 revision: None,
268 capabilities: None,
269 extensions: BTreeMap::new(),
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct FilesystemEntryPage {
276 pub entries: Vec<FilesystemEntry>,
277 pub next_token: Option<String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Default)]
281pub struct FilesystemMutationContext {
282 pub expected_revision: Option<String>,
283 pub expected_target_revision: Option<String>,
284}
285
286impl FilesystemMutationContext {
287 pub fn required(&self) -> bool {
288 self.expected_revision.is_some() || self.expected_target_revision.is_some()
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct FilesystemMutation {
294 pub path: String,
295 pub revision: Option<String>,
296 pub mount_revision: Option<String>,
297 pub extensions: BTreeMap<String, String>,
298}
299
300impl FilesystemMutation {
301 pub fn path(path: impl Into<String>) -> Self {
302 Self {
303 path: path.into(),
304 revision: None,
305 mount_revision: None,
306 extensions: BTreeMap::new(),
307 }
308 }
309}
310
311pub trait SynchronousFileProvider: FileProvider {}
312
313pub trait IFilesystem {
314 fn descriptor(&self) -> FilesystemDescriptor;
315
316 fn capabilities(&self) -> FilesystemCapabilities {
317 self.descriptor().capabilities().clone()
318 }
319
320 fn stat<'a>(
321 &'a self,
322 context: FilesystemCallContext,
323 path: String,
324 ) -> FilesystemFuture<'a, FilesystemEntry>;
325
326 fn read<'a>(
327 &'a self,
328 context: FilesystemCallContext,
329 path: String,
330 ) -> FilesystemFuture<'a, Vec<u8>>;
331
332 fn write<'a>(
333 &'a self,
334 context: FilesystemCallContext,
335 path: String,
336 bytes: Vec<u8>,
337 options: WriteOptions,
338 mutation: FilesystemMutationContext,
339 ) -> FilesystemFuture<'a, FilesystemMutation>;
340
341 fn entries_page<'a>(
342 &'a self,
343 context: FilesystemCallContext,
344 path: String,
345 request: FilesystemPageRequest,
346 ) -> FilesystemFuture<'a, FilesystemEntryPage>;
347
348 fn mkdir<'a>(
349 &'a self,
350 context: FilesystemCallContext,
351 path: String,
352 options: MkdirOptions,
353 mutation: FilesystemMutationContext,
354 ) -> FilesystemFuture<'a, FilesystemMutation>;
355
356 fn delete<'a>(
357 &'a self,
358 context: FilesystemCallContext,
359 path: String,
360 options: DeleteOptions,
361 mutation: FilesystemMutationContext,
362 ) -> FilesystemFuture<'a, FilesystemMutation>;
363
364 fn copy<'a>(
365 &'a self,
366 context: FilesystemCallContext,
367 source: String,
368 target: String,
369 options: CopyOptions,
370 mutation: FilesystemMutationContext,
371 ) -> FilesystemFuture<'a, FilesystemMutation>;
372
373 fn move_entry<'a>(
374 &'a self,
375 context: FilesystemCallContext,
376 source: String,
377 target: String,
378 options: MoveOptions,
379 mutation: FilesystemMutationContext,
380 ) -> FilesystemFuture<'a, FilesystemMutation>;
381
382 fn close<'a>(&'a self, context: FilesystemCallContext) -> FilesystemFuture<'a, ()>;
383}
384
385#[path = "providers.rs"]
386pub mod providers;
387#[cfg(not(target_arch = "wasm32"))]
388pub mod sftp;
389
390#[derive(Clone)]
391pub struct FilesystemHandle {
392 filesystem: Rc<dyn IFilesystem>,
393}
394
395impl std::fmt::Debug for FilesystemHandle {
396 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 formatter
398 .debug_struct("FilesystemHandle")
399 .field("descriptor", &self.descriptor())
400 .finish()
401 }
402}
403
404impl FilesystemHandle {
405 pub fn new<F: IFilesystem + 'static>(filesystem: F) -> Self {
406 Self {
407 filesystem: Rc::new(filesystem),
408 }
409 }
410
411 pub fn from_legacy<P: SynchronousFileProvider + 'static>(
412 provider: P,
413 descriptor: FilesystemDescriptor,
414 ) -> Self {
415 Self::new(LegacyFilesystem::new(provider, descriptor))
416 }
417
418 pub fn descriptor(&self) -> FilesystemDescriptor {
419 self.filesystem.descriptor()
420 }
421
422 pub fn capabilities(&self) -> FilesystemCapabilities {
423 self.filesystem.capabilities()
424 }
425
426 pub fn as_filesystem(&self) -> &dyn IFilesystem {
427 self.filesystem.as_ref()
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn call_context_cancellation_is_shared() {
437 let context = FilesystemCallContext::default();
438 let second = context.clone();
439 assert!(context.cancel());
440 assert!(second.cancelled());
441 assert!(matches!(second.check(), Err(FileError::Io(_))));
442 }
443
444 #[test]
445 fn descriptors_expose_only_explicit_redacted_fields() {
446 let descriptor = FilesystemDescriptor::new(
447 "github",
448 "hara-lang/hara@main",
449 false,
450 FilesystemCapabilities::new([
451 FilesystemCapability::Read,
452 FilesystemCapability::Entries,
453 FilesystemCapability::RevisionCheck,
454 ]),
455 )
456 .with_revision("commit-sha")
457 .with_extension("provider/ref", "heads/main");
458 assert_eq!(descriptor.kind(), "github");
459 assert_eq!(descriptor.display(), "hara-lang/hara@main");
460 assert_eq!(descriptor.revision(), Some("commit-sha"));
461 assert!(descriptor
462 .capabilities()
463 .contains(FilesystemCapability::RevisionCheck));
464 }
465}