modules/fs/
fs.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7#[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}