1pub mod directory;
33pub mod fault;
34pub mod file;
35pub mod locks;
36
37use std::cell::RefCell;
38
39use wasm_bindgen::JsCast as _;
40use web_sys::FileSystemDirectoryHandle;
41
42use blockworx_store::container::MANIFEST;
43use blockworx_store::lock::{Claim, Holding};
44use blockworx_store::record::WallTime;
45use blockworx_store::storage::{Entry, Name, Residency, Storage};
46
47use crate::directory::Depth;
48use crate::fault::{mistaken, settled};
49
50pub use crate::directory::Missing;
51
52#[derive(Clone)]
55pub struct Root(FileSystemDirectoryHandle);
56
57impl Root {
58 pub async fn open() -> std::io::Result<Self> {
62 let origin = web_sys::window()
63 .ok_or_else(|| {
64 std::io::Error::other(
65 "origin-private storage is reached from a page, and there is none",
66 )
67 })?
68 .navigator()
69 .storage();
70 let root = settled(origin.get_directory()).await?;
71 Ok(Self(
72 root.dyn_into().map_err(mistaken("the origin's root"))?,
73 ))
74 }
75
76 pub async fn container(&self, name: &Name, missing: Missing) -> std::io::Result<Opfs> {
87 Ok(Opfs {
88 dir: directory::directory(&self.0, name.as_str(), missing).await?,
89 root: self.0.clone(),
90 name: name.clone(),
91 held: RefCell::default(),
92 })
93 }
94
95 pub async fn containers(&self) -> std::io::Result<Vec<Name>> {
104 let mut containers = Vec::new();
105 for held in directory::directories(&self.0).await? {
106 let Some(name) = Name::new(&held) else {
107 continue;
108 };
109 let dir = directory::directory(&self.0, name.as_str(), Missing::IsNothing).await?;
110 if directory::holds(&dir, MANIFEST.as_str()).await? {
111 containers.push(name);
112 }
113 }
114 containers.sort();
115 Ok(containers)
116 }
117
118 pub async fn holds(&self, name: &Name) -> std::io::Result<bool> {
124 directory::holds(&self.0, name.as_str()).await
125 }
126
127 pub async fn remove(&self, name: &Name) -> std::io::Result<()> {
135 directory::remove(&self.0, name.as_str(), Depth::Everything).await
136 }
137
138 pub async fn remove_unheld(&self, name: &Name) -> std::io::Result<()> {
146 let Some(_held) = locks::request(&Opfs::lock_name(name)).await? else {
147 return Err(std::io::Error::new(
148 std::io::ErrorKind::ResourceBusy,
149 format!("{name} is open in another tab"),
150 ));
151 };
152 self.remove(name).await
153 }
154}
155
156pub struct Opfs {
158 root: FileSystemDirectoryHandle,
161 dir: FileSystemDirectoryHandle,
162 name: Name,
163 held: RefCell<Option<locks::Held>>,
164}
165
166impl Opfs {
167 async fn within(
170 &self,
171 at: &Entry,
172 missing: Missing,
173 ) -> std::io::Result<(FileSystemDirectoryHandle, String)> {
174 let path = segments(at);
175 let Some((name, holding)) = path.split_last() else {
176 return Err(std::io::Error::new(
177 std::io::ErrorKind::InvalidInput,
178 format!("{} is the container itself, not an entry in it", self.name),
179 ));
180 };
181 Ok((self.walk(holding, missing).await?, (*name).to_owned()))
182 }
183
184 async fn entry(
185 &self,
186 at: &Entry,
187 missing: Missing,
188 ) -> std::io::Result<web_sys::FileSystemFileHandle> {
189 let (dir, name) = self.within(at, missing).await?;
190 directory::file(&dir, &name, missing).await
191 }
192
193 async fn walk(
194 &self,
195 path: &[&str],
196 missing: Missing,
197 ) -> std::io::Result<FileSystemDirectoryHandle> {
198 let mut at = self.dir.clone();
199 for segment in path {
200 at = directory::directory(&at, segment, missing).await?;
201 }
202 Ok(at)
203 }
204
205 fn lock_name(name: &Name) -> String {
206 format!("blockworx/{name}")
207 }
208}
209
210impl std::fmt::Debug for Opfs {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(f, "{} in origin-private storage", self.name)
213 }
214}
215
216impl Storage for Opfs {
217 fn name(&self) -> Name {
218 self.name.clone()
219 }
220
221 fn names(&self, at: &Entry) -> String {
222 if at.as_str().is_empty() {
223 return self.name.to_string();
224 }
225 format!("{}/{at}", self.name)
226 }
227
228 fn residency(&self) -> Residency {
229 Residency::Resident
230 }
231
232 async fn read<'a>(&'a self, at: &'a Entry) -> std::io::Result<Option<Vec<u8>>> {
233 let Some(handle) = nothing_there(self.entry(at, Missing::IsNothing).await)? else {
234 return Ok(None);
235 };
236 file::read(&handle).await.map(Some)
237 }
238
239 async fn exists<'a>(&'a self, at: &'a Entry) -> std::io::Result<bool> {
244 match self.entry(at, Missing::IsNothing).await {
245 Ok(_) => Ok(true),
246 Err(why) if not_an_entry(&why) => Ok(false),
247 Err(why) => Err(why),
248 }
249 }
250
251 async fn write<'a>(&'a self, at: &'a Entry, bytes: &'a [u8]) -> std::io::Result<()> {
252 file::write(&self.entry(at, Missing::IsMade).await?, bytes).await
253 }
254
255 async fn append<'a>(&'a self, at: &'a Entry, bytes: &'a [u8]) -> std::io::Result<()> {
256 file::append(&self.entry(at, Missing::IsMade).await?, bytes).await
257 }
258
259 async fn truncate<'a>(&'a self, at: &'a Entry, len: u64) -> std::io::Result<()> {
260 file::truncate(&self.entry(at, Missing::IsMade).await?, len).await
261 }
262
263 async fn list<'a>(&'a self, dir: &'a Entry) -> std::io::Result<Vec<String>> {
264 let within = self.walk(&segments(dir), Missing::IsNothing).await?;
265 directory::names(&within).await
266 }
267
268 async fn remove<'a>(&'a self, at: &'a Entry) -> std::io::Result<()> {
269 let (dir, name) = self.within(at, Missing::IsNothing).await?;
270 directory::remove(&dir, &name, Depth::Entry).await
271 }
272
273 async fn create_dir<'a>(&'a self, dir: &'a Entry) -> std::io::Result<()> {
274 self.walk(&segments(dir), Missing::IsMade).await?;
275 Ok(())
276 }
277
278 async fn rename<'a>(&'a mut self, to: &'a Name) -> std::io::Result<()> {
287 if directory::holds(&self.root, to.as_str()).await? {
288 return Err(taken(to));
289 }
290 let holding = self.held.borrow().is_some();
291 let relocked = if holding {
292 Some(
293 locks::request(&Self::lock_name(to))
294 .await?
295 .ok_or_else(|| taken(to))?,
296 )
297 } else {
298 None
299 };
300
301 let moved = directory::directory(&self.root, to.as_str(), Missing::IsMade).await?;
302 directory::copy(&self.dir, &moved).await?;
303 directory::remove(&self.root, self.name.as_str(), Depth::Everything).await?;
304
305 if let Some(relocked) = relocked {
306 *self.held.borrow_mut() = Some(relocked);
307 }
308 self.dir = moved;
309 self.name = to.clone();
310 Ok(())
311 }
312
313 async fn discard(&self) -> std::io::Result<()> {
314 directory::remove(&self.root, self.name.as_str(), Depth::Entry).await
315 }
316
317 async fn claim(&self, _now: WallTime) -> std::io::Result<Claim> {
321 let Some(held) = locks::request(&Self::lock_name(&self.name)).await? else {
322 return Ok(Claim::Held(Holding::Elsewhere));
323 };
324 *self.held.borrow_mut() = Some(held);
325 Ok(Claim::Taken)
326 }
327
328 fn release(&self) {
329 drop(self.held.borrow_mut().take());
330 }
331}
332
333fn segments(at: &Entry) -> Vec<&str> {
336 at.as_str().split('/').filter(|of| !of.is_empty()).collect()
337}
338
339fn nothing_there<T>(got: std::io::Result<T>) -> std::io::Result<Option<T>> {
342 match got {
343 Ok(found) => Ok(Some(found)),
344 Err(why) if why.kind() == std::io::ErrorKind::NotFound => Ok(None),
345 Err(why) => Err(why),
346 }
347}
348
349fn taken(name: &Name) -> std::io::Error {
352 std::io::Error::new(
353 std::io::ErrorKind::AlreadyExists,
354 format!("{name} already exists"),
355 )
356}
357
358fn not_an_entry(why: &std::io::Error) -> bool {
361 matches!(
362 why.kind(),
363 std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
364 )
365}