1#[cfg(windows)]
8use std::os::windows::fs::MetadataExt as _;
9use std::path::{Path, PathBuf};
10use std::{fs, os};
11
12use ion::class::ClassObjectWrapper;
13use ion::flags::PropertyFlags;
14use ion::function::Opt;
15use ion::{ClassDefinition as _, Context, FromValue, Iterator, Object, Promise, Result, function_spec, js_fn};
16use mozjs::jsapi::{JSFunction, JSFunctionSpec, JSObject};
17use runtime::module::NativeModule;
18use runtime::promise::future_to_promise;
19use tokio::task::spawn_blocking;
20#[cfg(windows)]
21use windows::Win32::Storage::FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_FLAGS_AND_ATTRIBUTES};
22
23use crate::fs::dir::DirIterator;
24use crate::fs::{FileHandle, Metadata, base_error, dir_error, file_error, metadata_error, translate_error};
25
26#[derive(Copy, Clone, Debug, FromValue)]
27struct OpenOptions {
28 #[ion(default = true)]
29 read: bool,
30 #[ion(default)]
31 write: bool,
32 #[ion(default)]
33 append: bool,
34 #[ion(default)]
35 truncate: bool,
36 #[ion(default)]
37 create: bool,
38 #[ion(name = "createNew", default)]
39 create_new: bool,
40}
41
42impl OpenOptions {
43 fn into_std(self) -> fs::OpenOptions {
44 let mut options = fs::OpenOptions::new();
45
46 options
47 .read(self.read)
48 .write(self.write)
49 .append(self.append)
50 .truncate(self.truncate)
51 .create(self.create)
52 .create_new(self.create_new);
53
54 options
55 }
56
57 fn into_tokio(self) -> tokio::fs::OpenOptions {
58 let options = self.into_std();
59 tokio::fs::OpenOptions::from(options)
60 }
61}
62
63impl Default for OpenOptions {
64 fn default() -> OpenOptions {
65 OpenOptions {
66 read: true,
67 write: false,
68 append: false,
69 truncate: false,
70 create: false,
71 create_new: false,
72 }
73 }
74}
75
76#[js_fn]
77fn open(cx: &Context, path_str: String, Opt(options): Opt<OpenOptions>) -> Option<Promise<'_>> {
78 future_to_promise(cx, move |_| async move {
79 let path = Path::new(&path_str);
80 let options = options.unwrap_or_default().into_tokio();
81
82 match options.open(path).await {
83 Ok(file) => Ok(ClassObjectWrapper(Box::new(FileHandle::new(
84 &path_str,
85 file.into_std().await,
86 )))),
87 Err(err) => Err(file_error("open", &path_str, &err, ())),
88 }
89 })
90}
91
92#[js_fn]
93fn open_sync(cx: &Context, path_str: String, Opt(options): Opt<OpenOptions>) -> Result<*mut JSObject> {
94 let path = Path::new(&path_str);
95 let options = options.unwrap_or_default().into_std();
96
97 match options.open(path) {
98 Ok(file) => Ok(FileHandle::new_object(cx, Box::new(FileHandle::new(&path_str, file)))),
99 Err(err) => Err(file_error("open", &path_str, &err, ())),
100 }
101}
102
103#[js_fn]
104fn create(cx: &Context, path_str: String) -> Option<Promise<'_>> {
105 future_to_promise(cx, |_| async move {
106 let path = Path::new(&path_str);
107 let mut options = tokio::fs::OpenOptions::new();
108 options.read(true).write(true).truncate(true).create(true);
109
110 match options.open(path).await {
111 Ok(file) => Ok(ClassObjectWrapper(Box::new(FileHandle::new(
112 &path_str,
113 file.into_std().await,
114 )))),
115 Err(err) => Err(file_error("create", &path_str, &err, ())),
116 }
117 })
118}
119
120#[js_fn]
121fn create_sync(cx: &Context, path_str: String) -> Result<*mut JSObject> {
122 let path = Path::new(&path_str);
123 let mut options = fs::OpenOptions::new();
124 options.read(true).write(true).truncate(true).create(true);
125
126 match options.open(path) {
127 Ok(file) => Ok(FileHandle::new_object(cx, Box::new(FileHandle::new(&path_str, file)))),
128 Err(err) => Err(file_error("create", &path_str, &err, ())),
129 }
130}
131
132#[js_fn]
133fn metadata(cx: &Context, path_str: String) -> Option<Promise<'_>> {
134 future_to_promise(cx, |_| async move {
135 let path = Path::new(&path_str);
136 match tokio::fs::metadata(path).await {
137 Ok(meta) => Ok(Metadata::new(&meta)),
138 Err(err) => Err(metadata_error(&path_str, &err)),
139 }
140 })
141}
142
143#[js_fn]
144fn metadata_sync(path_str: String) -> Result<Metadata> {
145 let path = Path::new(&path_str);
146 match fs::metadata(path) {
147 Ok(meta) => Ok(Metadata::new(&meta)),
148 Err(err) => Err(metadata_error(&path_str, &err)),
149 }
150}
151
152#[js_fn]
153fn link_metadata(cx: &Context, path_str: String) -> Option<Promise<'_>> {
154 future_to_promise(cx, |_| async move {
155 let path = Path::new(&path_str);
156 match tokio::fs::symlink_metadata(path).await {
157 Ok(meta) => Ok(Metadata::new(&meta)),
158 Err(err) => Err(metadata_error(&path_str, &err)),
159 }
160 })
161}
162
163#[js_fn]
164fn link_metadata_sync(path_str: String) -> Result<Metadata> {
165 let path = Path::new(&path_str);
166 match fs::symlink_metadata(path) {
167 Ok(meta) => Ok(Metadata::new(&meta)),
168 Err(err) => Err(metadata_error(&path_str, &err)),
169 }
170}
171
172#[js_fn]
173fn read_dir(cx: &Context, path_str: String) -> Option<Promise<'_>> {
174 future_to_promise(cx, |_| async move {
175 let path = PathBuf::from(&path_str);
176
177 spawn_blocking(move || fs::read_dir(path))
178 .await
179 .unwrap()
180 .map(DirIterator::new_iterator)
181 .map_err(|err| dir_error("read", &path_str, &err))
182 })
183}
184
185#[js_fn]
186fn read_dir_sync(path_str: String) -> Result<Iterator> {
187 let path = Path::new(&path_str);
188
189 match fs::read_dir(path) {
190 Ok(dir) => Ok(DirIterator::new_iterator(dir)),
191 Err(err) => Err(dir_error("read", &path_str, &err)),
192 }
193}
194
195#[js_fn]
196fn create_dir(cx: &Context, path_str: String, Opt(recursive): Opt<bool>) -> Option<Promise<'_>> {
197 future_to_promise(cx, move |_| async move {
198 let path = Path::new(&path_str);
199 let recursive = recursive.unwrap_or_default();
200
201 let result = if recursive {
202 tokio::fs::create_dir_all(path).await
203 } else {
204 tokio::fs::create_dir(path).await
205 };
206 match result {
207 Ok(_) => Ok(()),
208 Err(err) => Err(dir_error("create", &path_str, &err)),
209 }
210 })
211}
212
213#[js_fn]
214fn create_dir_sync(path_str: String, Opt(recursive): Opt<bool>) -> Result<()> {
215 let path = Path::new(&path_str);
216 let recursive = recursive.unwrap_or_default();
217
218 let result = if recursive {
219 fs::create_dir_all(path)
220 } else {
221 fs::create_dir(path)
222 };
223 match result {
224 Ok(_) => Ok(()),
225 Err(err) => Err(dir_error("create", &path_str, &err)),
226 }
227}
228
229#[js_fn]
230fn remove(cx: &Context, path_str: String, Opt(recursive): Opt<bool>) -> Option<Promise<'_>> {
231 future_to_promise(cx, move |_| async move {
232 let path = Path::new(&path_str);
233 let recursive = recursive.unwrap_or_default();
234
235 let metadata = tokio::fs::symlink_metadata(path).await?;
236 let file_type = metadata.file_type();
237
238 let result = if file_type.is_dir() {
239 if recursive {
240 tokio::fs::remove_dir_all(path).await
241 } else {
242 tokio::fs::remove_dir(path).await
243 }
244 } else {
245 #[cfg(unix)]
246 {
247 tokio::fs::remove_file(path).await
248 }
249
250 #[cfg(windows)]
251 {
252 let attributes = FILE_FLAGS_AND_ATTRIBUTES(metadata.file_attributes());
253 if attributes.contains(FILE_ATTRIBUTE_DIRECTORY) {
254 tokio::fs::remove_dir(path).await
255 } else {
256 tokio::fs::remove_file(path).await
257 }
258 }
259 };
260 result.map_err(|err| base_error("remove", &path_str, &err))
261 })
262}
263
264#[js_fn]
265fn remove_sync(path_str: String, Opt(recursive): Opt<bool>) -> Result<()> {
266 let path = Path::new(&path_str);
267 let recursive = recursive.unwrap_or_default();
268
269 let metadata = fs::symlink_metadata(path)?;
270 let file_type = metadata.file_type();
271
272 let result = if file_type.is_dir() {
273 if recursive {
274 fs::remove_dir_all(path)
275 } else {
276 fs::remove_dir(path)
277 }
278 } else {
279 #[cfg(unix)]
280 {
281 fs::remove_file(path)
282 }
283
284 #[cfg(windows)]
285 {
286 let attributes = FILE_FLAGS_AND_ATTRIBUTES(metadata.file_attributes());
287 if attributes.contains(FILE_ATTRIBUTE_DIRECTORY) {
288 fs::remove_dir(path)
289 } else {
290 fs::remove_file(path)
291 }
292 }
293 };
294 result.map_err(|err| base_error("remove", &path_str, &err))
295}
296
297#[js_fn]
298fn copy(cx: &Context, from_str: String, to_str: String) -> Option<Promise<'_>> {
299 future_to_promise(cx, |_| async move {
300 let from = Path::new(&from_str);
301 let to = Path::new(&to_str);
302
303 tokio::fs::copy(from, to)
304 .await
305 .map_err(|err| translate_error("copy from", &from_str, &to_str, &err))
306 })
307}
308
309#[js_fn]
310fn copy_sync(from_str: String, to_str: String) -> Result<u64> {
311 let from = Path::new(&from_str);
312 let to = Path::new(&to_str);
313
314 fs::copy(from, to).map_err(|err| translate_error("copy from", &from_str, &to_str, &err))
315}
316
317#[js_fn]
318fn rename(cx: &Context, from_str: String, to_str: String) -> Option<Promise<'_>> {
319 future_to_promise(cx, |_| async move {
320 let from = Path::new(&from_str);
321 let to = Path::new(&to_str);
322
323 tokio::fs::rename(from, to)
324 .await
325 .map_err(|err| translate_error("rename from", &from_str, &to_str, &err))
326 })
327}
328
329#[js_fn]
330fn rename_sync(from_str: String, to_str: String) -> Result<()> {
331 let from = Path::new(&from_str);
332 let to = Path::new(&to_str);
333
334 fs::rename(from, to).map_err(|err| translate_error("rename from", &from_str, &to_str, &err))
335}
336
337#[js_fn]
338fn symlink(cx: &Context, original_str: String, link_str: String) -> Option<Promise<'_>> {
339 future_to_promise(cx, |_| async move {
340 let original = Path::new(&original_str);
341 let link = Path::new(&link_str);
342
343 let result;
344 #[cfg(unix)]
345 {
346 result = tokio::fs::symlink(original, link).await;
347 }
348 #[cfg(windows)]
349 {
350 result = if original.is_dir() {
351 tokio::fs::symlink_dir(original, link).await
352 } else {
353 tokio::fs::symlink_file(original, link).await
354 };
355 }
356 result.map_err(|err| translate_error("symlink", &original_str, &link_str, &err))
357 })
358}
359
360#[js_fn]
361fn symlink_sync(original_str: String, link_str: String) -> Result<()> {
362 let original = Path::new(&original_str);
363 let link = Path::new(&link_str);
364
365 let result;
366 #[cfg(target_family = "unix")]
367 {
368 result = os::unix::fs::symlink(original, link);
369 }
370 #[cfg(target_family = "windows")]
371 {
372 result = if original.is_dir() {
373 os::windows::fs::symlink_dir(original, link)
374 } else {
375 os::windows::fs::symlink_file(original, link)
376 };
377 }
378 result.map_err(|err| translate_error("symlink", &original_str, &link_str, &err))
379}
380
381#[js_fn]
382fn link(cx: &Context, original_str: String, link_str: String) -> Option<Promise<'_>> {
383 future_to_promise(cx, |_| async move {
384 let original = Path::new(&original_str);
385 let link = Path::new(&link_str);
386
387 tokio::fs::hard_link(original, link)
388 .await
389 .map_err(|err| translate_error("link", &original_str, &link_str, &err))
390 })
391}
392
393#[js_fn]
394fn link_sync(original_str: String, link_str: String) -> Result<()> {
395 let original = Path::new(&original_str);
396 let link = Path::new(&link_str);
397
398 fs::hard_link(original, link).map_err(|err| translate_error("link", &original_str, &link_str, &err))
399}
400
401#[js_fn]
402fn read_link(cx: &Context, path_str: String) -> Option<Promise<'_>> {
403 future_to_promise(cx, |_| async move {
404 let path = Path::new(&path_str);
405
406 match tokio::fs::read_link(&path).await {
407 Ok(path) => Ok(path.to_string_lossy().into_owned()),
408 Err(err) => Err(base_error("read link", &path_str, &err)),
409 }
410 })
411}
412
413#[js_fn]
414fn read_link_sync(path_str: String) -> Result<String> {
415 let path = Path::new(&path_str);
416
417 match fs::read_link(path) {
418 Ok(path) => Ok(path.to_string_lossy().into_owned()),
419 Err(err) => Err(base_error("read link", &path_str, &err)),
420 }
421}
422
423#[js_fn]
424fn canonical(cx: &Context, path_str: String) -> Option<Promise<'_>> {
425 future_to_promise(cx, |_| async move {
426 let path = Path::new(&path_str);
427
428 match tokio::fs::canonicalize(&path).await {
429 Ok(path) => Ok(path.to_string_lossy().into_owned()),
430 Err(err) => Err(base_error("read link", &path_str, &err)),
431 }
432 })
433}
434
435#[js_fn]
436fn canonical_sync(path_str: String) -> Result<String> {
437 let path = Path::new(&path_str);
438
439 match fs::canonicalize(path) {
440 Ok(path) => Ok(path.to_string_lossy().into_owned()),
441 Err(err) => Err(base_error("read link", &path_str, &err)),
442 }
443}
444
445const SYNC_FUNCTIONS: &[JSFunctionSpec] = &[
446 function_spec!(open_sync, c"open", 1),
447 function_spec!(create_sync, c"create", 1),
448 function_spec!(metadata, c"metadata", 1),
449 function_spec!(link_metadata, c"linkMetadata", 1),
450 function_spec!(read_dir_sync, c"readDir", 1),
451 function_spec!(create_dir_sync, c"createDir", 1),
452 function_spec!(remove_sync, c"remove", 1),
453 function_spec!(copy_sync, c"copy", 2),
454 function_spec!(rename_sync, c"rename", 2),
455 function_spec!(symlink_sync, c"symlink", 2),
456 function_spec!(link_sync, c"link", 2),
457 function_spec!(read_link_sync, c"readLink", 1),
458 function_spec!(canonical_sync, c"canonical", 1),
459 JSFunctionSpec::ZERO,
460];
461
462const ASYNC_FUNCTIONS: &[JSFunctionSpec] = &[
463 function_spec!(open, 1),
464 function_spec!(create, 1),
465 function_spec!(metadata, 1),
466 function_spec!(link_metadata, c"linkMetadata", 1),
467 function_spec!(read_dir, c"readDir", 1),
468 function_spec!(create_dir, c"createDir", 1),
469 function_spec!(remove, c"remove", 1),
470 function_spec!(copy, 2),
471 function_spec!(rename, 2),
472 function_spec!(symlink, c"symlink", 2),
473 function_spec!(link, c"link", 2),
474 function_spec!(read_link, c"readLink", 1),
475 function_spec!(canonical, c"canonical", 1),
476 JSFunctionSpec::ZERO,
477];
478
479pub struct FileSystemSync;
480
481impl<'cx> NativeModule<'cx> for FileSystemSync {
482 const NAME: &'static str = "fs/sync";
483 const VARIABLE_NAME: &'static str = "fsSync";
484 const SOURCE: &'static str = include_str!("fs_sync.js");
485
486 fn module(&self, cx: &'cx Context) -> Option<Object<'cx>> {
487 let fs = Object::new(cx);
488
489 unsafe { fs.define_methods(cx, SYNC_FUNCTIONS) }.then_some(fs)
490 }
491}
492
493pub struct FileSystem<'cx> {
494 pub sync: &'cx Object<'cx>,
495}
496
497impl<'cx> NativeModule<'cx> for FileSystem<'cx> {
498 const NAME: &'static str = "fs";
499 const VARIABLE_NAME: &'static str = "fs";
500 const SOURCE: &'static str = include_str!("fs.js");
501
502 fn module(&self, cx: &'cx Context) -> Option<Object<'cx>> {
503 let fs = Object::new(cx);
504
505 let mut result = unsafe { fs.define_methods(cx, ASYNC_FUNCTIONS) }
506 && fs.define_as(cx, "sync", &self.sync, PropertyFlags::CONSTANT_ENUMERATED)
507 && FileHandle::init_class(cx, &fs).0;
508
509 macro_rules! key {
510 ($key:literal) => {
511 ($key, concat!($key, "Sync"))
512 };
513 }
514 const SYNC_KEYS: [(&str, &str); 11] = [
515 key!("open"),
516 key!("create"),
517 key!("readDir"),
518 key!("createDir"),
519 key!("remove"),
520 key!("copy"),
521 key!("rename"),
522 key!("symlink"),
523 key!("link"),
524 key!("readLink"),
525 key!("canonical"),
526 ];
527
528 for (key, new_key) in SYNC_KEYS {
529 let function = self.sync.get_as::<_, *mut JSFunction>(cx, key, true, ()).ok().flatten();
530 result = result
531 && function.is_some()
532 && fs.define_as(cx, new_key, &function.unwrap(), PropertyFlags::CONSTANT_ENUMERATED);
533 }
534
535 result.then_some(fs)
536 }
537}