1use color_eyre::eyre::WrapErr;
19use itertools::Itertools;
20
21use crate::{DirEntry, Metadata, OpenFlags, Result, StdIoErrorExt};
22use pin_project::pin_project;
23use std::{
24 io::ErrorKind::{InvalidInput, PermissionDenied},
25 pin::Pin,
26 task::Poll,
27};
28
29#[derive(Debug, Clone)]
30pub struct FileSystem {
31 root_path: camino::Utf8PathBuf,
32}
33
34#[derive(Debug)]
35#[pin_project]
36pub struct File {
37 file: Inner,
38 path: camino::Utf8PathBuf,
39 stripped_path: Option<camino::Utf8PathBuf>,
40 #[pin]
41 async_file: async_fs::File,
42}
43
44#[derive(Debug)]
45enum Inner {
46 StdFsFile(std::fs::File),
47 NamedTempFile(tempfile::NamedTempFile),
48}
49
50impl FileSystem {
51 pub fn new(root_path: impl AsRef<camino::Utf8Path>) -> Self {
52 Self {
53 root_path: root_path.as_ref().to_path_buf(),
54 }
55 }
56
57 pub fn root_path(&self) -> &camino::Utf8Path {
58 &self.root_path
59 }
60
61 pub async fn from_folder_picker() -> Result<Self> {
62 let c = "While picking a folder from the host filesystem";
63 if let Some(path) = rfd::AsyncFileDialog::default().pick_folder().await {
64 let path = camino::Utf8Path::from_path(path.path())
65 .ok_or(crate::Error::PathUtf8Error)
66 .wrap_err(c)?;
67 Ok(Self::new(path))
68 } else {
69 Err(crate::Error::CancelledLoading).wrap_err(c)
70 }
71 }
72
73 pub async fn from_file_picker() -> Result<Self> {
74 let c = "While picking a folder from the host filesystem";
75 if let Some(path) = rfd::AsyncFileDialog::default()
76 .add_filter("project file", &["rxproj", "rvproj", "rvproj2", "lumproj"])
77 .pick_file()
78 .await
79 {
80 let path = camino::Utf8Path::from_path(path.path())
81 .ok_or(crate::Error::PathUtf8Error)
82 .wrap_err(c)?
83 .parent()
84 .expect("path does not have parent");
85 Ok(Self::new(path))
86 } else {
87 Err(crate::Error::CancelledLoading).wrap_err(c)
88 }
89 }
90}
91
92impl crate::FileSystem for FileSystem {
93 type File = File;
94
95 fn open_file(
96 &self,
97 path: impl AsRef<camino::Utf8Path>,
98 flags: OpenFlags,
99 ) -> Result<Self::File> {
100 let stripped_path = path.as_ref();
101 let path = self.root_path.join(path.as_ref());
102 let c = format!("While opening file {path:?} in a host folder");
103 std::fs::OpenOptions::new()
104 .create(flags.contains(OpenFlags::Create))
105 .write(flags.contains(OpenFlags::Write))
106 .read(flags.contains(OpenFlags::Read))
107 .truncate(flags.contains(OpenFlags::Truncate))
108 .open(&path)
109 .map(|file| {
110 let clone = file.try_clone().wrap_err_with(|| c.clone())?;
111 Ok(File {
112 file: Inner::StdFsFile(file),
113 path,
114 stripped_path: Some(stripped_path.to_owned()),
115 async_file: clone.into(),
116 })
117 })
118 .wrap_err_with(|| c.clone())?
119 }
120
121 fn metadata(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Metadata> {
122 let c = format!(
123 "While getting metadata for {:?} in a host folder",
124 path.as_ref()
125 );
126 let path = self.root_path.join(path);
127 let metadata = std::fs::metadata(path).wrap_err_with(|| c.clone())?;
128 Ok(Metadata {
129 is_file: metadata.is_file(),
130 size: metadata.len(),
131 })
132 }
133
134 fn rename(
135 &self,
136 from: impl AsRef<camino::Utf8Path>,
137 to: impl AsRef<camino::Utf8Path>,
138 ) -> Result<()> {
139 let c = format!(
140 "While renaming {:?} to {:?} in a host folder",
141 from.as_ref(),
142 to.as_ref()
143 );
144 let from = self.root_path.join(from);
145 let to = self.root_path.join(to);
146 std::fs::rename(from, to).wrap_err(c)
147 }
148
149 fn exists(&self, path: impl AsRef<camino::Utf8Path>) -> Result<bool> {
150 let c = format!(
151 "While checking if {:?} exists in a host folder",
152 path.as_ref()
153 );
154 let path = self.root_path.join(path);
155 path.try_exists().wrap_err(c)
156 }
157
158 fn create_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()> {
159 let c = format!(
160 "While creating a directory at {:?} in a host folder",
161 path.as_ref()
162 );
163 let path = self.root_path.join(path);
164 std::fs::create_dir_all(path).wrap_err(c)
165 }
166
167 fn remove_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()> {
168 let c = format!(
169 "While removing a directory at {:?} in a host folder",
170 path.as_ref()
171 );
172 let path = self.root_path.join(path);
173 std::fs::remove_dir_all(path).wrap_err(c)
174 }
175
176 fn remove_file(&self, path: impl AsRef<camino::Utf8Path>) -> Result<()> {
177 let c = format!(
178 "While removing a file at {:?} in a host folder",
179 path.as_ref()
180 );
181 let path = self.root_path.join(path);
182 std::fs::remove_file(path).wrap_err(c)
183 }
184
185 fn read_dir(&self, path: impl AsRef<camino::Utf8Path>) -> Result<Vec<DirEntry>> {
186 let c = format!(
187 "While reading the contents of the directory {:?} in a host folder",
188 path.as_ref()
189 );
190 let path = self.root_path.join(path);
191 path.read_dir_utf8()
192 .wrap_err_with(|| c.clone())?
193 .map_ok(|entry| {
194 let path = entry.into_path();
195 let path = path
196 .strip_prefix(&self.root_path)
197 .unwrap_or(&path)
198 .to_path_buf();
199
200 #[cfg(windows)]
202 let path = path.into_string().replace('\\', "/").into();
203
204 let metadata = self.metadata(&path).wrap_err_with(|| c.clone())?;
205 Ok(DirEntry::new(path, metadata))
206 })
207 .flatten()
208 .try_collect()
209 }
210}
211
212impl File {
213 pub fn new() -> std::io::Result<Self> {
215 let c = "While creating a temporary file on the host filesystem";
216 let file = tempfile::NamedTempFile::new()?;
217 let path = file
218 .path()
219 .to_str()
220 .ok_or(std::io::Error::new(
221 InvalidInput,
222 "Tried to create a temporary file, but its path was not valid UTF-8",
223 ))
224 .wrap_io_err(c)?
225 .into();
226 let clone = file.as_file().try_clone().wrap_io_err(c)?;
227 Ok(Self {
228 file: Inner::NamedTempFile(file),
229 path,
230 stripped_path: None,
231 async_file: clone.into(),
232 })
233 }
234
235 pub async fn from_file_picker(
242 filter_name: &str,
243 extensions: &[impl ToString],
244 ) -> Result<(Self, String)> {
245 let c = "While picking a file on the host filesystem";
246 if let Some(path) = rfd::AsyncFileDialog::default()
247 .add_filter(filter_name, extensions)
248 .pick_file()
249 .await
250 {
251 let file = std::fs::OpenOptions::new()
252 .read(true)
253 .open(path.path())
254 .map_err(crate::Error::IoError)
255 .wrap_err(c)?;
256 let path = path
257 .path()
258 .iter()
259 .last()
260 .unwrap()
261 .to_os_string()
262 .into_string()
263 .map_err(|_| crate::Error::PathUtf8Error)
264 .wrap_err(c)?;
265 let clone = file.try_clone().wrap_err(c)?;
266 Ok((
267 File {
268 file: Inner::StdFsFile(file),
269 path: path.clone().into(),
270 stripped_path: Some(
271 camino::Utf8Path::new(&path)
272 .iter()
273 .next_back()
274 .unwrap()
275 .into(),
276 ),
277 async_file: clone.into(),
278 },
279 path,
280 ))
281 } else {
282 Err(crate::Error::CancelledLoading).wrap_err(c)
283 }
284 }
285
286 pub async fn save(&self, filename: &str, filter_name: &str) -> Result<()> {
304 let stripped_path = self
305 .stripped_path
306 .as_ref()
307 .map(|p| p.as_str())
308 .unwrap_or("<temporary file>");
309 let c = format!(
310 "While saving the file {:?} in a host folder to disk",
311 stripped_path
312 );
313 let mut dialog = rfd::AsyncFileDialog::default().set_file_name(filename);
314 if let Some((_, extension)) = filename.rsplit_once('.') {
315 dialog = dialog.add_filter(filter_name, &[extension]);
316 }
317 let path = dialog
318 .save_file()
319 .await
320 .ok_or(crate::Error::CancelledLoading)
321 .wrap_err_with(|| c.clone())?;
322 std::fs::copy(&self.path, path.path()).wrap_err_with(|| c.clone())?;
323 Ok(())
324 }
325}
326
327impl crate::File for File {
328 fn metadata(&self) -> std::io::Result<Metadata> {
329 let stripped_path = self
330 .stripped_path
331 .as_ref()
332 .map(|p| p.as_str())
333 .unwrap_or("<temporary file>");
334 let c = format!(
335 "While getting metadata for file {:?} in a host folder",
336 stripped_path
337 );
338 let metdata = self.file.as_file().metadata().wrap_io_err(c)?;
339 Ok(Metadata {
340 is_file: metdata.is_file(),
341 size: metdata.len(),
342 })
343 }
344
345 fn set_len(&self, new_size: u64) -> std::io::Result<()> {
346 let stripped_path = self
347 .stripped_path
348 .as_ref()
349 .map(|p| p.as_str())
350 .unwrap_or("<temporary file>");
351 let c = format!(
352 "While setting length of file {:?} in a host folder",
353 stripped_path
354 );
355 self.file.as_file().set_len(new_size).wrap_io_err(c)
356 }
357}
358
359impl std::io::Read for File {
360 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
361 let stripped_path = self
362 .stripped_path
363 .as_ref()
364 .map(|p| p.as_str())
365 .unwrap_or("<temporary file>");
366 let c = format!(
367 "While reading from file {:?} in a host folder",
368 stripped_path
369 );
370 self.file.as_file().read(buf).wrap_io_err(c)
371 }
372
373 fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
374 let stripped_path = self
375 .stripped_path
376 .as_ref()
377 .map(|p| p.as_str())
378 .unwrap_or("<temporary file>");
379 let c = format!(
380 "While reading (vectored) from file {:?} in a host folder",
381 stripped_path
382 );
383 self.file.as_file().read_vectored(bufs).wrap_io_err(c)
384 }
385}
386
387impl futures_lite::AsyncRead for File {
388 fn poll_read(
389 self: std::pin::Pin<&mut Self>,
390 cx: &mut std::task::Context<'_>,
391 buf: &mut [u8],
392 ) -> Poll<std::io::Result<usize>> {
393 let stripped_path = self
394 .stripped_path
395 .as_ref()
396 .map(|p| p.as_str())
397 .unwrap_or("<temporary file>");
398 let c = format!(
399 "While asynchronously reading from file {:?} in a host folder",
400 stripped_path
401 );
402 self.project()
403 .async_file
404 .poll_read(cx, buf)
405 .map(|p| p.wrap_io_err(c))
406 }
407
408 fn poll_read_vectored(
409 self: Pin<&mut Self>,
410 cx: &mut std::task::Context<'_>,
411 bufs: &mut [std::io::IoSliceMut<'_>],
412 ) -> Poll<std::io::Result<usize>> {
413 let stripped_path = self
414 .stripped_path
415 .as_ref()
416 .map(|p| p.as_str())
417 .unwrap_or("<temporary file>");
418 let c = format!(
419 "While asynchronously reading (vectored) from file {:?} in a host folder",
420 stripped_path
421 );
422 self.project()
423 .async_file
424 .poll_read_vectored(cx, bufs)
425 .map(|p| p.wrap_io_err(c))
426 }
427}
428
429impl std::io::Write for File {
430 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
431 let stripped_path = self
432 .stripped_path
433 .as_ref()
434 .map(|p| p.as_str())
435 .unwrap_or("<temporary file>");
436 let c = format!("While writing to file {:?} in a host folder", stripped_path);
437 self.file.as_file().write(buf).wrap_io_err(c)
438 }
439
440 fn flush(&mut self) -> std::io::Result<()> {
441 let stripped_path = self
442 .stripped_path
443 .as_ref()
444 .map(|p| p.as_str())
445 .unwrap_or("<temporary file>");
446 let c = format!("While flushing file {:?} in a host folder", stripped_path);
447 self.file.as_file().flush().wrap_io_err(c)
448 }
449
450 fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
451 let stripped_path = self
452 .stripped_path
453 .as_ref()
454 .map(|p| p.as_str())
455 .unwrap_or("<temporary file>");
456 let c = format!(
457 "While writing (vectored) to file {:?} in a host folder",
458 stripped_path
459 );
460 self.file.as_file().write_vectored(bufs).wrap_io_err(c)
461 }
462}
463
464impl futures_lite::AsyncWrite for File {
465 fn poll_write(
466 self: Pin<&mut Self>,
467 cx: &mut std::task::Context<'_>,
468 buf: &[u8],
469 ) -> Poll<std::io::Result<usize>> {
470 let stripped_path = self
471 .stripped_path
472 .as_ref()
473 .map(|p| p.as_str())
474 .unwrap_or("<temporary file>");
475 let c = format!(
476 "While asynchronously writing to file {:?} in a host folder",
477 stripped_path
478 );
479 self.project()
480 .async_file
481 .poll_write(cx, buf)
482 .map(|r| r.wrap_io_err(c))
483 }
484
485 fn poll_write_vectored(
486 self: Pin<&mut Self>,
487 cx: &mut std::task::Context<'_>,
488 bufs: &[std::io::IoSlice<'_>],
489 ) -> Poll<std::io::Result<usize>> {
490 let stripped_path = self
491 .stripped_path
492 .as_ref()
493 .map(|p| p.as_str())
494 .unwrap_or("<temporary file>");
495 let c = format!(
496 "While asynchronously writing (vectored) to file {:?} in a host folder",
497 stripped_path
498 );
499 self.project()
500 .async_file
501 .poll_write_vectored(cx, bufs)
502 .map(|r| r.wrap_io_err(c))
503 }
504
505 fn poll_flush(
506 self: Pin<&mut Self>,
507 cx: &mut std::task::Context<'_>,
508 ) -> Poll<std::io::Result<()>> {
509 let stripped_path = self
510 .stripped_path
511 .as_ref()
512 .map(|p| p.as_str())
513 .unwrap_or("<temporary file>");
514 let c = format!(
515 "While asynchronously flushing file {:?} in a host folder",
516 stripped_path
517 );
518 self.project()
519 .async_file
520 .poll_flush(cx)
521 .map(|r| r.wrap_io_err(c))
522 }
523
524 fn poll_close(
525 self: Pin<&mut Self>,
526 _cx: &mut std::task::Context<'_>,
527 ) -> Poll<std::io::Result<()>> {
528 let stripped_path = self
529 .stripped_path
530 .as_ref()
531 .map(|p| p.as_str())
532 .unwrap_or("<temporary file>");
533 let c = format!(
534 "While asynchronously closing file {:?} in a host folder",
535 stripped_path
536 );
537 Poll::Ready(Err(std::io::Error::new(PermissionDenied, "Attempted to asynchronously close a `luminol_filesystem::host::File`, which is not allowed")).wrap_io_err(c))
538 }
539}
540
541impl std::io::Seek for File {
542 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
543 let stripped_path = self
544 .stripped_path
545 .as_ref()
546 .map(|p| p.as_str())
547 .unwrap_or("<temporary file>");
548 let c = format!("While seeking file {:?} in a host folder", stripped_path);
549 self.file.as_file().seek(pos).wrap_io_err(c)
550 }
551}
552
553impl futures_lite::AsyncSeek for File {
554 fn poll_seek(
555 self: Pin<&mut Self>,
556 cx: &mut std::task::Context<'_>,
557 pos: std::io::SeekFrom,
558 ) -> Poll<std::io::Result<u64>> {
559 let stripped_path = self
560 .stripped_path
561 .as_ref()
562 .map(|p| p.as_str())
563 .unwrap_or("<temporary file>");
564 let c = format!(
565 "While asynchronously seeking file {:?} in a host folder",
566 stripped_path
567 );
568 self.project()
569 .async_file
570 .poll_seek(cx, pos)
571 .map(|r| r.wrap_io_err(c))
572 }
573}
574
575impl Inner {
576 fn as_file(&self) -> &std::fs::File {
577 match self {
578 Inner::StdFsFile(file) => file,
579 Inner::NamedTempFile(file) => file.as_file(),
580 }
581 }
582}