mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 20:19:39 +00:00
Migration to new Rust-based engine (#401)
* wasm first steps * parsing start * parsing continued * progress * progress * progress * progress * progress * progress * progress * progress * fixes * reorganize * change type * fix * progress * refactoring * progress * progress * progress * keep style info * progress * progress * remove (copy) in name * progress * merge methods into 1 * splice * progress * progress * progress * cleaning * progress * progress * progress * visibility * progress * progress * progress * progress * progress * fixes * progress * fix markers * fix * progress * progress * progress * progress * progress * progress * small fix * use engine info * fix popups * small fix * progress * progress * progress * clean tool * handle self closing elements * reduce data exchanges across boundary * copy on wasm side to avoid any data corruption * speed fix * routing * refactoring * reverse * reverse * crop * split * merge * progress * time tool * fix bounds for waypoints * reduce * fix reactivity * handle corrupt time data * export * progress * remove gpx js package * remove todo * update readme * cleaning * progress * fix link fields * remove unused fields * refactoring and tests * import and export of routes, ref #192 * per-chunk categories storage * cross-tab sync * outdated state * refactoring * reorder methods * refactoring * fix * improve flow * renaming * progress * progress * fix * progress * per file history * keep original flag * use history variant * fix * progress * file explorer * progress * fix * sticky folders * improved sticky folders * fix sortable * fix sortable * fix * fix tab size * no file delete on desktop * fix routing on other file * fix sorting * remember language * open links in browser * commit missing changes * avoid selecting on resize * clarify filename * explorer actions * fix folder styling * drop anywhere in folder * cleaning * fixes * remove focus-ring * full screen * small fix * consistent highlight color * improved preview * performance improvements * sparser data transfers * sparser data transfers * show popup once data is loaded * fix routing control size * fix * sparse desktop build * remove custom ign styles * use new endpoint for openhikingmap * strip native menu * reorder context menu items * route full support * route - track conversion * create route file action * route export, filter out anchors at high zoom level * clarify name handling * move hide & center to view * routes continued * update docs * fix power format, closes #378 * lint fixes * fix display of additional data with gaps, closes #361 * fix linting issues * fix import modules * fix multidrag * change base to resolve * fix type errors * fix layer typings * fix last type error * each keys * improve typings * deploy to cloudflare --------- Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -0,0 +1,285 @@
|
||||
//! The storage of the engine on the file system: what the engine holds is kept between sessions,
|
||||
//! in a directory of the app (see `gpx_engine::Storage`).
|
||||
//!
|
||||
//! ```text
|
||||
//! head the revision of the last commit
|
||||
//! order the order of the files
|
||||
//! files/<id> the record of each file
|
||||
//! trackpoints/<id> the chunks of trackpoints
|
||||
//! waypoints/<id> the chunks of waypoints
|
||||
//! settings.json the settings of the frontend
|
||||
//! ```
|
||||
//!
|
||||
//! A commit writes the chunks, the records and the order first, then the head, then deletes what
|
||||
//! is not used anymore: the head moves once all of the new data is there, and nothing that the old
|
||||
//! data refers to is gone before that.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use gpx_engine::{
|
||||
Batch, ChunkKey, ChunkKind, CommitError, FileId, Revision, Storage, StorageError, StoredData,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct FileStorage {
|
||||
root: PathBuf,
|
||||
settings: Mutex<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
fn failed(error: impl std::fmt::Display) -> StorageError {
|
||||
StorageError::Failed(error.to_string())
|
||||
}
|
||||
|
||||
/// Writes a file as a whole or not at all.
|
||||
pub fn write_atomically(path: &Path, bytes: &[u8]) -> io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let temporary = path.with_extension("tmp");
|
||||
fs::write(&temporary, bytes)?;
|
||||
fs::rename(&temporary, path)
|
||||
}
|
||||
|
||||
fn read_optional(path: &Path) -> io::Result<Option<Vec<u8>>> {
|
||||
match fs::read(path) {
|
||||
Ok(bytes) => Ok(Some(bytes)),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(path: &Path) -> io::Result<()> {
|
||||
match fs::remove_file(path) {
|
||||
Err(error) if error.kind() != io::ErrorKind::NotFound => Err(error),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl FileStorage {
|
||||
/// Opens the storage in `root`, which is created if it does not exist.
|
||||
pub fn open(root: impl Into<PathBuf>) -> Result<Self, StorageError> {
|
||||
let root = root.into();
|
||||
fs::create_dir_all(&root).map_err(failed)?;
|
||||
let settings = match read_optional(&root.join("settings.json")).map_err(failed)? {
|
||||
Some(bytes) => serde_json::from_slice(&bytes).map_err(failed)?,
|
||||
None => BTreeMap::new(),
|
||||
};
|
||||
Ok(Self {
|
||||
root,
|
||||
settings: Mutex::new(settings),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> BTreeMap<String, String> {
|
||||
self.settings.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn chunk_path(&self, key: &ChunkKey) -> PathBuf {
|
||||
let directory = match key.kind {
|
||||
ChunkKind::Trackpoints => "trackpoints",
|
||||
ChunkKind::Waypoints => "waypoints",
|
||||
};
|
||||
self.root.join(directory).join(key.id.to_string())
|
||||
}
|
||||
|
||||
fn file_path(&self, id: &FileId) -> PathBuf {
|
||||
self.root.join("files").join(id.0.to_string())
|
||||
}
|
||||
|
||||
/// The ids and contents of the files of a directory, named by UUIDs.
|
||||
fn read_directory(&self, name: &str) -> Result<Vec<(Uuid, Vec<u8>)>, StorageError> {
|
||||
let directory = self.root.join(name);
|
||||
let entries = match fs::read_dir(&directory) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(vec![]),
|
||||
Err(error) => return Err(failed(error)),
|
||||
};
|
||||
let mut items = vec![];
|
||||
for entry in entries {
|
||||
let path = entry.map_err(failed)?.path();
|
||||
let Some(id) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(|name| Uuid::parse_str(name).ok())
|
||||
else {
|
||||
// temporary files of a write that did not finish
|
||||
continue;
|
||||
};
|
||||
items.push((id, fs::read(&path).map_err(failed)?));
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn save_settings(&self, settings: &BTreeMap<String, String>) -> Result<(), StorageError> {
|
||||
let json = serde_json::to_vec(settings).map_err(failed)?;
|
||||
write_atomically(&self.root.join("settings.json"), &json).map_err(failed)
|
||||
}
|
||||
|
||||
fn read_head(&self) -> Result<Revision, StorageError> {
|
||||
match read_optional(&self.root.join("head")).map_err(failed)? {
|
||||
Some(bytes) => String::from_utf8_lossy(&bytes)
|
||||
.trim()
|
||||
.parse()
|
||||
.map(Revision)
|
||||
.map_err(failed),
|
||||
None => Ok(Revision::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_batch(&self, batch: &Batch, head: Revision) -> io::Result<()> {
|
||||
for (key, bytes) in &batch.put_chunks {
|
||||
write_atomically(&self.chunk_path(key), bytes)?;
|
||||
}
|
||||
for (id, bytes) in &batch.put_files {
|
||||
write_atomically(&self.file_path(id), bytes)?;
|
||||
}
|
||||
if let Some(order) = &batch.order {
|
||||
write_atomically(&self.root.join("order"), order)?;
|
||||
}
|
||||
write_atomically(&self.root.join("head"), head.0.to_string().as_bytes())?;
|
||||
for id in &batch.delete_files {
|
||||
remove(&self.file_path(id))?;
|
||||
}
|
||||
for key in &batch.delete_chunks {
|
||||
remove(&self.chunk_path(key))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for FileStorage {
|
||||
async fn load(&self) -> Result<StoredData, StorageError> {
|
||||
let chunks = |kind: ChunkKind, name: &str| -> Result<Vec<_>, StorageError> {
|
||||
Ok(self
|
||||
.read_directory(name)?
|
||||
.into_iter()
|
||||
.map(|(id, bytes)| (ChunkKey { kind, id }, bytes))
|
||||
.collect())
|
||||
};
|
||||
let mut all_chunks = chunks(ChunkKind::Trackpoints, "trackpoints")?;
|
||||
all_chunks.extend(chunks(ChunkKind::Waypoints, "waypoints")?);
|
||||
Ok(StoredData {
|
||||
files: self
|
||||
.read_directory("files")?
|
||||
.into_iter()
|
||||
.map(|(id, bytes)| (FileId(id), bytes))
|
||||
.collect(),
|
||||
chunks: all_chunks,
|
||||
order: read_optional(&self.root.join("order")).map_err(failed)?,
|
||||
head: self.read_head()?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn head(&self) -> Result<Revision, StorageError> {
|
||||
self.read_head()
|
||||
}
|
||||
|
||||
async fn commit(&self, batch: &Batch) -> Result<Revision, CommitError> {
|
||||
let head = self.read_head()?;
|
||||
if head != batch.expect_head {
|
||||
return Err(CommitError::Stale { head });
|
||||
}
|
||||
let next = head.next();
|
||||
self.write_batch(batch, next)
|
||||
.map_err(|error| CommitError::Failed(failed(error)))?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
async fn load_settings(&self) -> Result<Vec<(String, String)>, StorageError> {
|
||||
Ok(self
|
||||
.settings
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn put_setting(&self, key: &str, value: &str) -> Result<(), StorageError> {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
settings.insert(key.to_owned(), value.to_owned());
|
||||
self.save_settings(&settings)
|
||||
}
|
||||
|
||||
async fn delete_setting(&self, key: &str) -> Result<(), StorageError> {
|
||||
let mut settings = self.settings.lock().unwrap();
|
||||
if settings.remove(key).is_some() {
|
||||
self.save_settings(&settings)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Engine;
|
||||
use gpx_engine::{Action, Command, Load, LoadFiles, Persister};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn directory() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("gpx-studio-store-{}", Uuid::new_v4()))
|
||||
}
|
||||
|
||||
fn engine_with_a_file() -> Engine {
|
||||
let mut engine = Engine::default();
|
||||
let data = std::fs::read("../gpx-rs/data/simple.gpx").unwrap();
|
||||
engine
|
||||
.execute(Action::Edit(Command::LoadFiles(LoadFiles {
|
||||
files: vec![Load {
|
||||
data: &data,
|
||||
name: "simple",
|
||||
}],
|
||||
})))
|
||||
.unwrap();
|
||||
engine
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_the_files_and_the_settings_between_sessions() {
|
||||
let root = directory();
|
||||
let engine = engine_with_a_file();
|
||||
let order = engine.order().to_vec();
|
||||
|
||||
let storage = FileStorage::open(&root).unwrap();
|
||||
pollster::block_on(Persister::default().save(&storage, engine.snapshot())).unwrap();
|
||||
pollster::block_on(storage.put_setting("units", "\"metric\"")).unwrap();
|
||||
|
||||
// another session
|
||||
let storage = FileStorage::open(&root).unwrap();
|
||||
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
|
||||
let mut other = Engine::default();
|
||||
other.restore(restored);
|
||||
assert_eq!(other.order(), order);
|
||||
assert_eq!(
|
||||
other.file_statistics(&order[0]).unwrap().total_distance,
|
||||
engine.file_statistics(&order[0]).unwrap().total_distance
|
||||
);
|
||||
assert_eq!(storage.settings()["units"], "\"metric\"");
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refuses_a_commit_that_is_not_based_on_the_head() {
|
||||
let root = directory();
|
||||
let storage = FileStorage::open(&root).unwrap();
|
||||
let batch = Batch {
|
||||
order: Some(vec![]),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(pollster::block_on(storage.commit(&batch)), Ok(Revision(1)));
|
||||
assert_eq!(
|
||||
pollster::block_on(storage.commit(&batch)),
|
||||
Err(CommitError::Stale { head: Revision(1) })
|
||||
);
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user