1use super::filesystem_bridge::{
2 block_on_local, bridge_future, collect_entries, filesystem_entry_value, walk_paths, ValueFuture,
3};
4use crate::core::Value;
5use crate::file::{
6 logical_normalise, logical_resolve, CopyOptions, DeleteOptions, FileEntry, FileError,
7 FileProvider, MkdirOptions, MoveOptions, TempDirectoryOptions, TempFileOptions, WriteMode,
8 WriteOptions,
9};
10use crate::filesystem::{FilesystemCallContext, FilesystemHandle, FilesystemMutationContext};
11use crate::task::Promise;
12use std::cell::Cell;
13use std::rc::Rc;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16static PROVIDER_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
17const TEMP_ATTEMPTS: usize = 128;
18
19#[derive(Clone)]
24pub struct FilesystemRuntimeAdapter {
25 state: Rc<FilesystemRuntimeState>,
26}
27
28struct FilesystemRuntimeState {
29 handle: FilesystemHandle,
30 close_started: Cell<bool>,
31}
32
33impl FilesystemRuntimeAdapter {
34 pub fn new(handle: FilesystemHandle) -> Self {
35 Self {
36 state: Rc::new(FilesystemRuntimeState {
37 handle,
38 close_started: Cell::new(false),
39 }),
40 }
41 }
42
43 pub fn handle(&self) -> FilesystemHandle {
44 self.state.handle.clone()
45 }
46
47 pub fn close(&self) -> Promise {
48 if self.state.close_started.replace(true) {
49 let promise = Promise::new();
50 promise.resolve(Value::Nil);
51 return promise;
52 }
53 let context = FilesystemCallContext::default();
54 let handle = self.handle();
55 let future_context = context.clone();
56 bridge_future(
57 "file/close",
58 "/",
59 None,
60 context,
61 Box::pin(async move {
62 handle
63 .as_filesystem()
64 .close(future_context)
65 .await
66 .map(|()| Value::Nil)
67 }),
68 )
69 }
70
71 fn effect(
72 &self,
73 operation: &'static str,
74 path: String,
75 target: Option<String>,
76 future: ValueFuture,
77 context: FilesystemCallContext,
78 ) -> Promise {
79 bridge_future(operation, &path, target.as_deref(), context, future)
80 }
81}
82
83impl Drop for FilesystemRuntimeState {
84 fn drop(&mut self) {
85 if self.close_started.replace(true) {
86 return;
87 }
88 #[cfg(not(target_arch = "wasm32"))]
89 {
90 let context = FilesystemCallContext::default();
91 let future = self.handle.as_filesystem().close(context);
92 let _ = block_on_local(future);
93 }
94 }
95}
96
97impl FileProvider for FilesystemRuntimeAdapter {
98 fn read_bytes(&self, _path: &str) -> Result<Vec<u8>, FileError> {
99 Err(FileError::Unsupported)
100 }
101
102 fn write_bytes(
103 &self,
104 _path: &str,
105 _bytes: Vec<u8>,
106 _options: WriteOptions,
107 ) -> Result<String, FileError> {
108 Err(FileError::Unsupported)
109 }
110
111 fn exists_value(&self, _path: &str) -> Result<bool, FileError> {
112 Err(FileError::Unsupported)
113 }
114
115 fn stat_entry(&self, _path: &str) -> Result<FileEntry, FileError> {
116 Err(FileError::Unsupported)
117 }
118
119 fn entries_values(&self, _path: &str) -> Result<Vec<FileEntry>, FileError> {
120 Err(FileError::Unsupported)
121 }
122
123 fn mkdir_path(&self, _path: &str, _options: MkdirOptions) -> Result<String, FileError> {
124 Err(FileError::Unsupported)
125 }
126
127 fn delete_path(&self, _path: &str, _options: DeleteOptions) -> Result<String, FileError> {
128 Err(FileError::Unsupported)
129 }
130
131 fn copy_path(
132 &self,
133 _source: &str,
134 _target: &str,
135 _options: CopyOptions,
136 ) -> Result<String, FileError> {
137 Err(FileError::Unsupported)
138 }
139
140 fn move_path(
141 &self,
142 _source: &str,
143 _target: &str,
144 _options: MoveOptions,
145 ) -> Result<String, FileError> {
146 Err(FileError::Unsupported)
147 }
148
149 fn temp_file_path(
150 &self,
151 _parent: &str,
152 _options: TempFileOptions,
153 ) -> Result<String, FileError> {
154 Err(FileError::Unsupported)
155 }
156
157 fn temp_directory_path(
158 &self,
159 _parent: &str,
160 _options: TempDirectoryOptions,
161 ) -> Result<String, FileError> {
162 Err(FileError::Unsupported)
163 }
164
165 fn read(&self, path: &str) -> Result<Promise, FileError> {
166 let path = logical_normalise(path)?;
167 let context = FilesystemCallContext::default();
168 let future_context = context.clone();
169 let handle = self.handle();
170 let future_path = path.clone();
171 let future = Box::pin(async move {
172 handle
173 .as_filesystem()
174 .read(future_context, future_path)
175 .await
176 .map(Value::Bytes)
177 });
178 Ok(self.effect("file/read", path, None, future, context))
179 }
180
181 fn write(&self, path: &str, bytes: Vec<u8>) -> Result<Promise, FileError> {
182 self.write_with_options(path, bytes, WriteOptions::default())
183 }
184
185 fn write_with_options(
186 &self,
187 path: &str,
188 bytes: Vec<u8>,
189 options: WriteOptions,
190 ) -> Result<Promise, FileError> {
191 let path = logical_normalise(path)?;
192 let context = FilesystemCallContext::default();
193 let future_context = context.clone();
194 let handle = self.handle();
195 let future_path = path.clone();
196 let future = Box::pin(async move {
197 handle
198 .as_filesystem()
199 .write(
200 future_context,
201 future_path,
202 bytes,
203 options,
204 FilesystemMutationContext::default(),
205 )
206 .await
207 .map(|mutation| Value::String(mutation.path))
208 });
209 Ok(self.effect("file/write", path, None, future, context))
210 }
211
212 fn exists(&self, path: &str) -> Result<Promise, FileError> {
213 let path = logical_normalise(path)?;
214 let context = FilesystemCallContext::default();
215 let future_context = context.clone();
216 let handle = self.handle();
217 let future_path = path.clone();
218 let future = Box::pin(async move {
219 match handle
220 .as_filesystem()
221 .stat(future_context, future_path)
222 .await
223 {
224 Ok(_) => Ok(Value::Bool(true)),
225 Err(FileError::NotFound) => Ok(Value::Bool(false)),
226 Err(error) => Err(error),
227 }
228 });
229 Ok(self.effect("file/exists?", path, None, future, context))
230 }
231
232 fn stat(&self, path: &str) -> Result<Promise, FileError> {
233 let path = logical_normalise(path)?;
234 let context = FilesystemCallContext::default();
235 let future_context = context.clone();
236 let handle = self.handle();
237 let future_path = path.clone();
238 let future = Box::pin(async move {
239 handle
240 .as_filesystem()
241 .stat(future_context, future_path)
242 .await
243 .map(filesystem_entry_value)
244 });
245 Ok(self.effect("file/stat", path, None, future, context))
246 }
247
248 fn entries(&self, path: &str) -> Result<Promise, FileError> {
249 let path = logical_normalise(path)?;
250 let context = FilesystemCallContext::default();
251 let handle = self.handle();
252 let future_context = context.clone();
253 let future_path = path.clone();
254 let future = Box::pin(async move {
255 collect_entries(handle, future_context, future_path)
256 .await
257 .map(|entries| {
258 Value::Vector(entries.into_iter().map(filesystem_entry_value).collect())
259 })
260 });
261 Ok(self.effect("file/entries", path, None, future, context))
262 }
263
264 fn list(&self, path: &str) -> Result<Promise, FileError> {
265 let path = logical_normalise(path)?;
266 let context = FilesystemCallContext::default();
267 let handle = self.handle();
268 let future_context = context.clone();
269 let future_path = path.clone();
270 let future = Box::pin(async move {
271 collect_entries(handle, future_context, future_path)
272 .await
273 .map(|entries| {
274 Value::Vector(
275 entries
276 .into_iter()
277 .map(|entry| Value::String(entry.path))
278 .collect(),
279 )
280 })
281 });
282 Ok(self.effect("file/list", path, None, future, context))
283 }
284
285 fn walk(&self, path: &str) -> Result<Promise, FileError> {
286 let path = logical_normalise(path)?;
287 let context = FilesystemCallContext::default();
288 let handle = self.handle();
289 let future_context = context.clone();
290 let future_path = path.clone();
291 let future = Box::pin(async move {
292 walk_paths(handle, future_context, future_path)
293 .await
294 .map(|paths| Value::Vector(paths.into_iter().map(Value::String).collect()))
295 });
296 Ok(self.effect("file/walk", path, None, future, context))
297 }
298
299 fn mkdir(&self, path: &str) -> Result<Promise, FileError> {
300 self.mkdir_with_options(path, MkdirOptions::default())
301 }
302
303 fn mkdir_with_options(&self, path: &str, options: MkdirOptions) -> Result<Promise, FileError> {
304 let path = logical_normalise(path)?;
305 let context = FilesystemCallContext::default();
306 let future_context = context.clone();
307 let handle = self.handle();
308 let future_path = path.clone();
309 let future = Box::pin(async move {
310 handle
311 .as_filesystem()
312 .mkdir(
313 future_context,
314 future_path,
315 options,
316 FilesystemMutationContext::default(),
317 )
318 .await
319 .map(|mutation| Value::String(mutation.path))
320 });
321 Ok(self.effect("file/mkdir", path, None, future, context))
322 }
323
324 fn delete(&self, path: &str) -> Result<Promise, FileError> {
325 self.delete_with_options(path, DeleteOptions::default())
326 }
327
328 fn delete_with_options(
329 &self,
330 path: &str,
331 options: DeleteOptions,
332 ) -> Result<Promise, FileError> {
333 let path = logical_normalise(path)?;
334 let context = FilesystemCallContext::default();
335 let future_context = context.clone();
336 let handle = self.handle();
337 let future_path = path.clone();
338 let future = Box::pin(async move {
339 handle
340 .as_filesystem()
341 .delete(
342 future_context,
343 future_path,
344 options,
345 FilesystemMutationContext::default(),
346 )
347 .await
348 .map(|mutation| Value::String(mutation.path))
349 });
350 Ok(self.effect("file/delete", path, None, future, context))
351 }
352
353 fn copy(&self, source: &str, target: &str, options: CopyOptions) -> Result<Promise, FileError> {
354 let source = logical_normalise(source)?;
355 let target = logical_normalise(target)?;
356 let context = FilesystemCallContext::default();
357 let future_context = context.clone();
358 let handle = self.handle();
359 let future_source = source.clone();
360 let future_target = target.clone();
361 let future = Box::pin(async move {
362 handle
363 .as_filesystem()
364 .copy(
365 future_context,
366 future_source,
367 future_target,
368 options,
369 FilesystemMutationContext::default(),
370 )
371 .await
372 .map(|mutation| Value::String(mutation.path))
373 });
374 Ok(self.effect("file/copy", source, Some(target), future, context))
375 }
376
377 fn move_entry(
378 &self,
379 source: &str,
380 target: &str,
381 options: MoveOptions,
382 ) -> Result<Promise, FileError> {
383 let source = logical_normalise(source)?;
384 let target = logical_normalise(target)?;
385 let context = FilesystemCallContext::default();
386 let future_context = context.clone();
387 let handle = self.handle();
388 let future_source = source.clone();
389 let future_target = target.clone();
390 let future = Box::pin(async move {
391 handle
392 .as_filesystem()
393 .move_entry(
394 future_context,
395 future_source,
396 future_target,
397 options,
398 FilesystemMutationContext::default(),
399 )
400 .await
401 .map(|mutation| Value::String(mutation.path))
402 });
403 Ok(self.effect("file/move", source, Some(target), future, context))
404 }
405
406 fn temp_file(&self, parent: &str, options: TempFileOptions) -> Result<Promise, FileError> {
407 let parent = logical_normalise(parent)?;
408 let context = FilesystemCallContext::default();
409 let future_context = context.clone();
410 let handle = self.handle();
411 let future_parent = parent.clone();
412 let future = Box::pin(async move {
413 for _ in 0..TEMP_ATTEMPTS {
414 future_context.check()?;
415 let sequence = PROVIDER_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
416 let path = logical_resolve(
417 &future_parent,
418 &format!("{}-{sequence}{}", options.prefix, options.suffix),
419 )?;
420 match handle
421 .as_filesystem()
422 .write(
423 future_context.clone(),
424 path.clone(),
425 Vec::new(),
426 WriteOptions {
427 mode: WriteMode::Create,
428 parents: false,
429 },
430 FilesystemMutationContext::default(),
431 )
432 .await
433 {
434 Ok(_) => return Ok(Value::String(path)),
435 Err(FileError::AlreadyExists) => continue,
436 Err(error) => return Err(error),
437 }
438 }
439 Err(FileError::Io(
440 "temporary file attempts were exhausted".into(),
441 ))
442 });
443 Ok(self.effect("file/temp-file", parent, None, future, context))
444 }
445
446 fn temp_directory(
447 &self,
448 parent: &str,
449 options: TempDirectoryOptions,
450 ) -> Result<Promise, FileError> {
451 let parent = logical_normalise(parent)?;
452 let context = FilesystemCallContext::default();
453 let future_context = context.clone();
454 let handle = self.handle();
455 let future_parent = parent.clone();
456 let future = Box::pin(async move {
457 for _ in 0..TEMP_ATTEMPTS {
458 future_context.check()?;
459 let sequence = PROVIDER_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
460 let path =
461 logical_resolve(&future_parent, &format!("{}-{sequence}", options.prefix))?;
462 match handle
463 .as_filesystem()
464 .mkdir(
465 future_context.clone(),
466 path.clone(),
467 MkdirOptions {
468 parents: false,
469 exists_ok: false,
470 },
471 FilesystemMutationContext::default(),
472 )
473 .await
474 {
475 Ok(_) => return Ok(Value::String(path)),
476 Err(FileError::AlreadyExists) => continue,
477 Err(error) => return Err(error),
478 }
479 }
480 Err(FileError::Io(
481 "temporary directory attempts were exhausted".into(),
482 ))
483 });
484 Ok(self.effect("file/temp-directory", parent, None, future, context))
485 }
486}