Skip to main content

blockworx_opfs/
directory.rs

1//! Handles: what a directory holds, and how one is reached.
2//!
3//! Every call here is one File System Access API call plus the typed
4//! question it was asked — whether something missing is nothing or is made,
5//! and whether a removal takes what is under it.
6
7use js_sys::AsyncIterator;
8use wasm_bindgen::{JsCast as _, JsValue};
9use web_sys::{
10    FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions,
11    FileSystemGetFileOptions, FileSystemHandle, FileSystemHandleKind, FileSystemRemoveOptions,
12};
13
14use crate::fault::{faulted, mistaken, settled};
15
16/// What to do about a name nothing stands under.
17#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum Missing {
19    IsNothing,
20    IsMade,
21}
22
23/// Whether a removal takes what is under the name with it.
24#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25pub enum Depth {
26    Entry,
27    Everything,
28}
29
30impl Missing {
31    fn made(self) -> bool {
32        matches!(self, Missing::IsMade)
33    }
34}
35
36/// The directory called `name` in `within`.
37///
38/// # Errors
39/// [`std::io::ErrorKind::NotFound`] where nothing stands under `name` and
40/// nothing is to be made, or the browser's own refusal.
41pub async fn directory(
42    within: &FileSystemDirectoryHandle,
43    name: &str,
44    missing: Missing,
45) -> std::io::Result<FileSystemDirectoryHandle> {
46    let options = FileSystemGetDirectoryOptions::new();
47    options.set_create(missing.made());
48    let got = settled(within.get_directory_handle_with_options(name, &options)).await?;
49    got.dyn_into().map_err(mistaken("a directory"))
50}
51
52/// The file called `name` in `within`.
53///
54/// # Errors
55/// As [`directory`].
56pub async fn file(
57    within: &FileSystemDirectoryHandle,
58    name: &str,
59    missing: Missing,
60) -> std::io::Result<FileSystemFileHandle> {
61    let options = FileSystemGetFileOptions::new();
62    options.set_create(missing.made());
63    let got = settled(within.get_file_handle_with_options(name, &options)).await?;
64    got.dyn_into().map_err(mistaken("a file"))
65}
66
67/// # Errors
68/// As [`directory`], and [`std::io::ErrorKind::NotFound`] for a name
69/// nothing stands under.
70pub async fn remove(
71    within: &FileSystemDirectoryHandle,
72    name: &str,
73    depth: Depth,
74) -> std::io::Result<()> {
75    let options = FileSystemRemoveOptions::new();
76    options.set_recursive(matches!(depth, Depth::Everything));
77    settled(within.remove_entry_with_options(name, &options)).await?;
78    Ok(())
79}
80
81/// What `of` holds, by name.
82///
83/// # Errors
84/// The browser's refusal, or an iteration that answers something that is
85/// not a handle.
86pub async fn names(of: &FileSystemDirectoryHandle) -> std::io::Result<Vec<String>> {
87    Ok(held(of).await?.iter().map(FileSystemHandle::name).collect())
88}
89
90/// What `of` holds, as handles — the form the library reads a kind off.
91///
92/// # Errors
93/// As [`names`].
94pub async fn held(of: &FileSystemDirectoryHandle) -> std::io::Result<Vec<FileSystemHandle>> {
95    let mut holds = Vec::new();
96    for handle in walked(of.values()).await? {
97        holds.push(handle.dyn_into().map_err(mistaken("a handle"))?);
98    }
99    Ok(holds)
100}
101
102/// Whether `of` holds a directory called `name` — the question a rename
103/// asks before it moves anything.
104///
105/// # Errors
106/// As [`names`].
107pub async fn holds(of: &FileSystemDirectoryHandle, name: &str) -> std::io::Result<bool> {
108    Ok(held(of).await?.iter().any(|handle| handle.name() == name))
109}
110
111/// What `of` holds that is itself a directory, by name — which is every
112/// container in the origin's root, and whatever else is down there.
113///
114/// # Errors
115/// As [`names`].
116pub async fn directories(of: &FileSystemDirectoryHandle) -> std::io::Result<Vec<String>> {
117    Ok(held(of)
118        .await?
119        .iter()
120        .filter(|handle| handle.kind() == FileSystemHandleKind::Directory)
121        .map(FileSystemHandle::name)
122        .collect())
123}
124
125/// Drain an async iterator, which is how a directory answers what is in it.
126async fn walked(over: AsyncIterator) -> std::io::Result<Vec<JsValue>> {
127    let mut walked = Vec::new();
128    loop {
129        let step = settled(over.next().map_err(faulted)?).await?;
130        if js_sys::Reflect::get(&step, &JsValue::from_str("done"))
131            .map_err(faulted)?
132            .is_truthy()
133        {
134            return Ok(walked);
135        }
136        walked.push(js_sys::Reflect::get(&step, &JsValue::from_str("value")).map_err(faulted)?);
137    }
138}
139
140/// Copy everything `from` holds into `into`, directories and all.
141///
142/// Which is what a rename in origin-private storage is: only Chromium has
143/// `FileSystemHandle.move()`, so the bytes travel and the old name is
144/// removed behind them.
145///
146/// # Errors
147/// As [`names`], and whatever reading or writing an entry refused.
148pub async fn copy(
149    from: &FileSystemDirectoryHandle,
150    into: &FileSystemDirectoryHandle,
151) -> std::io::Result<()> {
152    let mut pending = vec![(from.clone(), into.clone())];
153    while let Some((from, into)) = pending.pop() {
154        for handle in held(&from).await? {
155            let name = handle.name();
156            if handle.kind() == FileSystemHandleKind::Directory {
157                pending.push((
158                    directory(&from, &name, Missing::IsNothing).await?,
159                    directory(&into, &name, Missing::IsMade).await?,
160                ));
161                continue;
162            }
163            let bytes = crate::file::read(&file(&from, &name, Missing::IsNothing).await?).await?;
164            crate::file::write(&file(&into, &name, Missing::IsMade).await?, &bytes).await?;
165        }
166    }
167    Ok(())
168}