Web files: designs live in the browser (OPFS), files come in by picker or drop and go out as downloads
- solvecraft_io::vfs: the engine's file access (disk natively; in the browser an in-memory set of files the host loads and stores). File commands and write_atomic go through it. - Web host (apps/solvecraft-web/src/files.rs): stored designs are read from OPFS at start (storage.persist() asked for) and written back when a command writes one; Save/Save As keep designs in the browser, exports (STEP, STL, 3MF, OBJ, ...) download as files. - File > Open shows "Files in this browser": open, insert, rename, download, delete, plus open or insert from the computer (showOpenFilePicker where there is one, an <input type=file> otherwise). Files dropped on the page are inserted or opened. - Unsaved work is autosaved to OPFS and offered again after a reload or crash. - Start page recents come from the stored designs, with thumbnails. In the browser the samples are built when opened (a picture blocks the page there, and a kernel panic can't be caught). - Pages without OPFS (http on a network address, old browsers) keep designs in local storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d191ae571e
commit
c9ed3d154d
13 files changed
+964
-42
No files matched your search
@@ -10,6 +10,7 @@ pub mod safe_write;
|
||||
mod sketch2d;
|
||||
mod svg;
|
||||
mod threemf;
|
||||
pub mod vfs;
|
||||
|
||||
pub use dxf::{read_dxf, write_dxf};
|
||||
pub use safe_write::{backup_path, write_atomic};
|
||||
|
||||
@@ -87,6 +87,11 @@ fn write_steps(path: &Path, bytes: &[u8], backup: bool, stop: Option<Step>) -> s
|
||||
/// Write `bytes` to `path` atomically; with `backup`, the file being replaced is kept as
|
||||
/// `<path>.bak` first.
|
||||
pub fn write_atomic(path: &Path, bytes: &[u8], backup: bool) -> std::io::Result<()> {
|
||||
// In the browser the host stores files (one write is already atomic there).
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
let _ = backup;
|
||||
return crate::vfs::write(&path.to_string_lossy(), bytes);
|
||||
}
|
||||
write_steps(path, bytes, backup, None)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! File access for the engine. Natively it is the disk. In the browser (wasm32) there is no
|
||||
//! file system to call synchronously, so files live in memory here: the web host loads them
|
||||
//! (from the browser's private storage, a file picker or a drop) before a command reads them,
|
||||
//! and takes what commands wrote ([`take_writes`]) to store it or hand it out as a download.
|
||||
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
pub fn read(path: &str) -> Result<Vec<u8>> {
|
||||
std::fs::read(path)
|
||||
}
|
||||
pub fn write(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
std::fs::write(path, bytes)
|
||||
}
|
||||
pub fn len(path: &str) -> Result<u64> {
|
||||
std::fs::metadata(path).map(|m| m.len())
|
||||
}
|
||||
pub fn exists(path: &str) -> bool {
|
||||
std::path::Path::new(path).exists()
|
||||
}
|
||||
pub fn remove(path: &str) -> Result<()> {
|
||||
std::fs::remove_file(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
thread_local! {
|
||||
static FILES: RefCell<BTreeMap<String, Vec<u8>>> = const { RefCell::new(BTreeMap::new()) };
|
||||
static WRITES: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
fn missing(path: &str) -> Error {
|
||||
Error::new(ErrorKind::NotFound, format!("{path}: no such file in this browser"))
|
||||
}
|
||||
|
||||
pub fn read(path: &str) -> Result<Vec<u8>> {
|
||||
FILES.with(|f| f.borrow().get(path).cloned()).ok_or_else(|| missing(path))
|
||||
}
|
||||
pub fn write(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
FILES.with(|f| f.borrow_mut().insert(path.to_string(), bytes.to_vec()));
|
||||
WRITES.with(|w| {
|
||||
let mut w = w.borrow_mut();
|
||||
w.retain(|p| p != path);
|
||||
w.push(path.to_string());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
pub fn len(path: &str) -> Result<u64> {
|
||||
FILES.with(|f| f.borrow().get(path).map(|b| b.len() as u64)).ok_or_else(|| missing(path))
|
||||
}
|
||||
pub fn exists(path: &str) -> bool {
|
||||
FILES.with(|f| f.borrow().contains_key(path))
|
||||
}
|
||||
pub fn remove(path: &str) -> Result<()> {
|
||||
FILES.with(|f| f.borrow_mut().remove(path)).map(|_| ()).ok_or_else(|| missing(path))
|
||||
}
|
||||
pub fn insert(path: &str, bytes: Vec<u8>) {
|
||||
FILES.with(|f| f.borrow_mut().insert(path.to_string(), bytes));
|
||||
}
|
||||
pub fn take_writes() -> Vec<(String, Vec<u8>)> {
|
||||
let paths = WRITES.with(|w| std::mem::take(&mut *w.borrow_mut()));
|
||||
paths.into_iter().filter_map(|p| FILES.with(|f| f.borrow().get(&p).cloned()).map(|b| (p, b))).collect()
|
||||
}
|
||||
pub fn list(prefix: &str) -> Vec<(String, u64)> {
|
||||
FILES.with(|f| f.borrow().iter().filter(|(p, _)| p.starts_with(prefix)).map(|(p, b)| (p.clone(), b.len() as u64)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub use imp::*;
|
||||
|
||||
/// A file's text.
|
||||
pub fn read_to_string(path: &str) -> Result<String> {
|
||||
String::from_utf8(read(path)?).map_err(|e| Error::new(ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn files_round_trip() {
|
||||
let dir = std::env::temp_dir().join(format!("solvecraft-vfs-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let p = dir.join("a.txt").to_string_lossy().to_string();
|
||||
assert!(!super::exists(&p));
|
||||
assert!(super::read(&p).is_err());
|
||||
super::write(&p, b"hello").unwrap();
|
||||
assert!(super::exists(&p));
|
||||
assert_eq!(super::len(&p).unwrap(), 5);
|
||||
assert_eq!(super::read_to_string(&p).unwrap(), "hello");
|
||||
super::remove(&p).unwrap();
|
||||
assert!(!super::exists(&p));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user