Skip to main content

hara_native/runtime/
filesystem_adapter.rs

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/// Routes the existing `std.native.File` promise API through an asynchronous
20/// provider-neutral [`FilesystemHandle`]. Synchronous primitive methods remain
21/// unavailable so remote and browser providers are never weakened to blocking
22/// result semantics.
23#[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}