1use blockworx_opfs::{Missing, Root};
16use blockworx_store::container::{Container, Discarded, Glance, discard_unclaimed};
17use blockworx_store::doc::Doc;
18use blockworx_store::handle::{Clock, Store};
19use blockworx_store::history::humanize;
20use blockworx_store::naming::{candidates, entropy};
21use blockworx_store::record::WallTime;
22use blockworx_store::storage::{Any, DocumentRef, Name};
23use blockworx_store::{recent, transfer};
24use core::time::Duration;
25use std::cell::RefCell;
26
27#[derive(Clone, PartialEq, Debug)]
29pub struct Listed {
30 pub name: String,
32 pub glance: Option<Glance>,
35}
36
37impl Listed {
38 #[must_use]
40 pub fn document(&self) -> String {
41 blockworx_editor::import::file_stem(&self.name)
42 }
43
44 #[must_use]
47 pub fn edited(&self, now: WallTime) -> Option<String> {
48 let glance = self.glance?;
49 let rev = glance.rev.get();
50 Some(
51 match now.unix_millis().checked_sub(glance.written.unix_millis()) {
52 Some(elapsed) => format!(
53 "Edited {} \u{00b7} rev {rev}",
54 humanize(Duration::from_millis(elapsed))
55 ),
56 None => format!("rev {rev}"),
59 },
60 )
61 }
62}
63
64pub struct Opened {
66 pub library: Library,
67 pub doc: Doc,
68 pub notice: Option<String>,
72}
73
74pub struct Library {
76 root: Option<Root>,
80 recent: RefCell<Vec<DocumentRef>>,
81 kept: crate::prefs::Store,
82}
83
84impl Opened {
85 #[must_use]
88 pub fn detached() -> Self {
89 Opened {
90 library: Library {
91 root: None,
92 recent: RefCell::new(Vec::new()),
93 kept: crate::prefs::Store::default(),
94 },
95 doc: Doc::default(),
96 notice: None,
97 }
98 }
99}
100
101impl Library {
102 pub async fn opening(kept: crate::prefs::Store) -> Opened {
109 let remembered = kept.recent();
110 let root = match Root::open().await {
111 Ok(root) => root,
112 Err(why) => {
113 return Opened {
114 library: Library {
115 root: None,
116 recent: RefCell::new(remembered),
117 kept,
118 },
119 doc: Doc::default(),
120 notice: Some(format!(
121 "Nothing will be saved: this page has no private storage ({why}). \
122 A page served over https or from localhost has one."
123 )),
124 };
125 }
126 };
127 let library = Library {
128 root: Some(root),
129 recent: RefCell::new(remembered),
130 kept,
131 };
132 let (doc, notice) = library.stands_on().await;
133 library.sweeps_unclaimed().await;
136 Opened {
137 library,
138 doc,
139 notice,
140 }
141 }
142
143 pub(crate) async fn stands_on(&self) -> (Doc, Option<String>) {
147 match self.born().await {
148 Ok(store) => (Doc::attached(store), None),
149 Err(why) => (Doc::default(), Some(why)),
150 }
151 }
152
153 async fn sweeps_unclaimed(&self) {
161 let Some(root) = &self.root else {
162 return;
163 };
164 let held = match root.containers().await {
165 Ok(held) => held,
166 Err(why) => {
167 tracing::error!("the origin would not list its documents: {why}");
168 return;
169 }
170 };
171 for name in held {
172 let storage = match root.container(&name, Missing::IsNothing).await {
173 Ok(storage) => storage,
174 Err(why) => {
175 tracing::warn!("{name} could not be looked at: {why}");
176 continue;
177 }
178 };
179 match discard_unclaimed(&storage, blockworx_store::history::now()).await {
180 Ok(Discarded::Removed) => tracing::info!("removed {name}, which held no edit"),
181 Ok(Discarded::Kept) => {}
182 Err(why) => tracing::warn!("{name} could not be tidied away: {why}"),
183 }
184 }
185 }
186
187 pub async fn containers(&self) -> Vec<Name> {
190 let Some(root) = &self.root else {
191 return Vec::new();
192 };
193 let mut held = root.containers().await.unwrap_or_else(|why| {
194 tracing::error!("the origin would not list its documents: {why}");
195 Vec::new()
196 });
197 let recent = self.recent.borrow();
198 held.sort_by_key(|name| {
199 recent
200 .iter()
201 .position(|named| named.as_str() == name.as_str())
202 .unwrap_or(recent.len())
203 });
204 held
205 }
206
207 pub async fn listing(&self) -> Vec<Listed> {
211 let Some(root) = &self.root else {
212 return Vec::new();
213 };
214 let mut listed = Vec::new();
215 for name in self.containers().await {
216 let glance = match root.container(&name, Missing::IsNothing).await {
217 Ok(storage) => Container::glance(storage).await.unwrap_or_else(|why| {
218 tracing::warn!("{name} could not be read: {why}");
219 None
220 }),
221 Err(why) => {
222 tracing::warn!("{name} could not be looked at: {why}");
223 None
224 }
225 };
226 listed.push(Listed {
227 name: name.to_string(),
228 glance,
229 });
230 }
231 listed.sort_by(|a, b| {
232 let written = |entry: &Listed| entry.glance.map(|at| at.written);
233 written(b).cmp(&written(a))
234 });
235 listed
236 }
237
238 pub async fn born(&self) -> Result<Store<Any>, String> {
244 let root = self.origin()?;
245 for called in candidates(entropy) {
246 match root.holds(&called).await {
247 Ok(true) => continue,
248 Ok(false) => {}
249 Err(why) => return Err(failed_to("create a new diagram", &why)),
250 }
251 let storage = root
252 .container(&called, Missing::IsMade)
253 .await
254 .map_err(|why| failed_to("create a new diagram", &why))?;
255 return Store::creating(Any::new(storage), Clock::System)
256 .await
257 .map_err(|why| failed_to("create a new diagram", &why));
258 }
259 Err("Failed to create a new diagram: no unused name to give it".to_owned())
260 }
261
262 pub async fn opens(&self, named: &DocumentRef) -> Result<Store<Any>, String> {
274 let root = self.origin()?;
275 let called =
276 Name::new(named.as_str()).ok_or_else(|| format!("{named} is not a diagram"))?;
277 let storage = root
278 .container(&called, Missing::IsNothing)
279 .await
280 .map_err(|why| why.to_string())?;
281 Store::opening(Any::new(storage), Clock::System)
282 .await
283 .map_err(|why| why.to_string())
284 }
285
286 pub async fn renames(&self, doc: &mut Doc, to: &str) -> Result<(), String> {
292 let was = doc
293 .container_name()
294 .ok_or("This session has no diagram to rename")?;
295 let called =
296 Name::of_document(to).ok_or_else(|| format!("\u{201c}{to}\u{201d} is not a name"))?;
297 if called == was {
298 return Ok(());
299 }
300 doc.renaming_to(&called)
301 .await
302 .map_err(|why| failed_to(&format!("rename {was}"), &why))?;
303 self.forgets(&DocumentRef::new(was.as_str()));
304 self.remembers(&called);
305 Ok(())
306 }
307
308 pub async fn removes(&self, named: &Name) -> Result<(), String> {
313 let root = self.origin()?;
314 root.remove(named)
315 .await
316 .map_err(|why| failed_to(&format!("delete {named}"), &why))?;
317 self.forgets(&DocumentRef::new(named.as_str()));
318 Ok(())
319 }
320
321 pub async fn removes_closed(&self, named: &Name) -> Result<(), String> {
327 let root = self.origin()?;
328 root.remove_unheld(named)
329 .await
330 .map_err(|why| failed_to(&format!("delete {named}"), &why))?;
331 self.forgets(&DocumentRef::new(named.as_str()));
332 Ok(())
333 }
334
335 pub async fn packs(&self, named: &Name) -> Result<Vec<u8>, String> {
345 let root = self.origin()?;
346 let storage = root
347 .container(named, Missing::IsNothing)
348 .await
349 .map_err(|why| failed_to(&format!("export {named}"), &why))?;
350 transfer::pack(&storage)
351 .await
352 .map_err(|why| failed_to(&format!("export {named}"), &why))
353 }
354
355 pub async fn unpacks(&self, called: &str, archive: &[u8]) -> Result<Store<Any>, String> {
365 let root = self.origin()?;
366 let named =
367 carried(called).ok_or_else(|| format!("\u{201c}{called}\u{201d} is not a diagram"))?;
368 let taken = root
369 .holds(&named)
370 .await
371 .map_err(|why| failed_to(&format!("import {named}"), &why))?;
372 if taken {
373 return Err(format!(
374 "Failed to import {named}: this origin already holds a diagram of that name"
375 ));
376 }
377 let storage = root
378 .container(&named, Missing::IsMade)
379 .await
380 .map_err(|why| failed_to(&format!("import {named}"), &why))?;
381 let laid = transfer::unpack(&storage, archive).await;
385 let opened = match laid {
386 Ok(()) => Store::opening(Any::new(storage), Clock::System)
387 .await
388 .map_err(|why| failed_to(&format!("import {named}"), &why)),
389 Err(why) => Err(failed_to(&format!("import {named}"), &why)),
390 };
391 if opened.is_err() {
392 let _ = root.remove(&named).await;
393 }
394 opened
395 }
396
397 pub fn remembers(&self, named: &Name) {
399 let mut recent = self.recent.borrow_mut();
400 recent::remember(&mut recent, &DocumentRef::new(named.as_str()));
401 self.kept.remembers(&recent);
402 }
403
404 fn forgets(&self, named: &DocumentRef) {
405 let mut recent = self.recent.borrow_mut();
406 recent::forget(&mut recent, named);
407 self.kept.remembers(&recent);
408 }
409
410 fn origin(&self) -> Result<&Root, String> {
411 self.root
412 .as_ref()
413 .ok_or_else(|| "This page has no private storage to keep a diagram in".to_owned())
414 }
415}
416
417pub fn picks_an_archive(shell: &crate::shell::Shell) {
423 use wasm_bindgen::{JsCast as _, prelude::Closure};
424
425 let Some(input) = crate::shell::window()
426 .and_then(|window| window.document())
427 .and_then(|document| document.create_element("input").ok())
428 .and_then(|element| element.dyn_into::<web_sys::HtmlInputElement>().ok())
429 else {
430 return;
431 };
432 input.set_type("file");
433 input.set_accept(".zip");
434 let shell = shell.clone();
435 let picked = input.clone();
436 let taken = Closure::once_into_js(move || {
437 let Some(file) = picked.files().and_then(|files| files.get(0)) else {
438 return;
439 };
440 let called = file.name();
441 crate::shell::spawn(async move {
442 match wasm_bindgen_futures::JsFuture::from(file.array_buffer()).await {
443 Ok(read) => {
444 shell.imports_archive(called, js_sys::Uint8Array::new(&read).to_vec());
445 }
446 Err(why) => shell.failed(format!("Could not read {called}: {why:?}")),
447 }
448 });
449 });
450 input.set_onchange(Some(taken.unchecked_ref()));
451 input.click();
452}
453
454fn carried(file: &str) -> Option<Name> {
455 Name::of_archive(file)
456}
457
458fn failed_to(what: &str, why: &impl std::fmt::Display) -> String {
459 format!("Failed to {what}: {why}")
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn named(file: &str) -> String {
467 carried(file)
468 .map(|name| name.to_string())
469 .unwrap_or_default()
470 }
471
472 #[test]
473 fn a_picked_archive_names_the_document_it_carries() {
474 assert_eq!(named("engine.bwx.zip"), "engine.bwx");
475 assert_eq!(named("engine.bwx"), "engine.bwx");
476 assert_eq!(named("engine.zip"), "engine.bwx");
477 assert_eq!(
478 named("notes.v2.bwx.zip"),
479 "notes.v2.bwx",
480 "a dotted document name keeps all of itself",
481 );
482 assert_eq!(carried("../escaped.zip"), None, "and a path is no name");
483 }
484
485 #[test]
489 fn a_listed_row_says_when_it_was_edited_and_at_which_rev() {
490 let now = WallTime::from_unix_millis(10 * 3_600_000);
491 let listed = |written| Listed {
492 name: "rig.bwx".to_owned(),
493 glance: Some(Glance {
494 rev: blockworx_doc::fixtures::rev(12),
495 written,
496 }),
497 };
498 assert_eq!(
499 listed(WallTime::from_unix_millis(7 * 3_600_000))
500 .edited(now)
501 .as_deref(),
502 Some("Edited 3 hours ago \u{00b7} rev 12"),
503 );
504 assert_eq!(
505 listed(WallTime::from_unix_millis(11 * 3_600_000))
506 .edited(now)
507 .as_deref(),
508 Some("rev 12"),
509 );
510 let fresh = Listed {
511 name: "rig.bwx".to_owned(),
512 glance: None,
513 };
514 assert_eq!(fresh.edited(now), None);
515 assert_eq!(fresh.document(), "rig");
516 }
517
518 #[test]
521 fn a_library_with_no_origin_refuses_every_door_in_words() {
522 let library = Library {
523 root: None,
524 recent: RefCell::new(Vec::new()),
525 kept: crate::prefs::Store::default(),
526 };
527 let Err(refusal) = library.origin() else {
528 panic!("a library with no root has no origin to answer");
529 };
530 assert!(refusal.contains("no private storage"), "{refusal}");
531 }
532}