⎇ 30467600bcd9effa15e8338fb32d357c1edab660
00_unanchay/pluma/pluma-notebook-store/src/lib.rs
History
Who touched what
raw
1//! `pluma-notebook-store` — save/load atómico de un [`Notebook`] a disco.
2//!
3//! Formato: JSON con header de versión de esquema. La escritura es atómica
4//! (write → fsync → rename), así que un crash a media escritura no corrompe
5//! el archivo final. Convención de extensión: `.pluma-nb`.
6
7#![forbid(unsafe_code)]
8
9use std::fs::{self, File};
10use std::io::{BufReader, BufWriter, Write};
11use std::path::{Path, PathBuf};
12
13use pluma_notebook_core::Notebook;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17/// Versión actual del esquema en disco. Subir solo ante cambios de formato
18/// incompatibles; la lectura rechaza versiones desconocidas.
19pub const SCHEMA: u32 = 1;
20
21#[derive(Debug, Error)]
22pub enum Error {
23 #[error("I/O: {0}")]
24 Io(#[from] std::io::Error),
25 #[error("JSON: {0}")]
26 Json(#[from] serde_json::Error),
27 #[error("versión de esquema desconocida: {found} (esta build soporta {SCHEMA})")]
28 SchemaDesconocida { found: u32 },
29}
30
31pub type Result<T> = std::result::Result<T, Error>;
32
33#[derive(Debug, Serialize, Deserialize)]
34struct Envelope {
35 schema: u32,
36 notebook: Notebook,
37}
38
39/// Carga un notebook desde un archivo `.pluma-nb`.
40pub fn load(ruta: &Path) -> Result<Notebook> {
41 let f = File::open(ruta)?;
42 let env: Envelope = serde_json::from_reader(BufReader::new(f))?;
43 if env.schema != SCHEMA {
44 return Err(Error::SchemaDesconocida { found: env.schema });
45 }
46 Ok(env.notebook)
47}
48
49/// Guarda un notebook de forma atómica: escribe a `<ruta>.tmp`, hace fsync,
50/// y renombra. Si algo falla a mitad, el archivo original queda intacto.
51pub fn save(ruta: &Path, notebook: &Notebook) -> Result<()> {
52 let env = Envelope { schema: SCHEMA, notebook: notebook.clone() };
53 let tmp = tmp_path(ruta);
54
55 {
56 let f = File::create(&tmp)?;
57 let mut w = BufWriter::new(f);
58 serde_json::to_writer_pretty(&mut w, &env)?;
59 w.flush()?;
60 w.into_inner()
61 .map_err(|e| std::io::Error::other(e.to_string()))?
62 .sync_all()?;
63 }
64
65 fs::rename(&tmp, ruta)?;
66 Ok(())
67}
68
69fn tmp_path(ruta: &Path) -> PathBuf {
70 let mut s = ruta.as_os_str().to_owned();
71 s.push(".tmp");
72 PathBuf::from(s)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use pluma_notebook_core::CellKind;
79
80 fn notebook_ejemplo() -> Notebook {
81 let mut nb = Notebook::new();
82 let a = nb.push(CellKind::Markdown, "# titulo");
83 let b = nb.push(CellKind::Code { language: "rust".into() }, "let x = 1;");
84 nb.add_dependency(b, a);
85 nb
86 }
87
88 #[test]
89 fn save_load_roundtrip_preserva_digest() {
90 let dir = tempfile::tempdir().unwrap();
91 let ruta = dir.path().join("ejemplo.pluma-nb");
92 let original = notebook_ejemplo();
93 save(&ruta, &original).unwrap();
94 let cargado = load(&ruta).unwrap();
95 assert_eq!(original.notebook_digest(), cargado.notebook_digest());
96 assert_eq!(original.len(), cargado.len());
97 }
98
99 #[test]
100 fn save_es_atomico_no_deja_tmp() {
101 let dir = tempfile::tempdir().unwrap();
102 let ruta = dir.path().join("at.pluma-nb");
103 save(&ruta, ¬ebook_ejemplo()).unwrap();
104 assert!(ruta.exists());
105 assert!(!tmp_path(&ruta).exists());
106 }
107
108 #[test]
109 fn schema_desconocida_falla() {
110 let dir = tempfile::tempdir().unwrap();
111 let ruta = dir.path().join("futuro.pluma-nb");
112 fs::write(&ruta, r#"{"schema": 999, "notebook": {"cells": [], "next_id": 1}}"#).unwrap();
113 assert!(matches!(load(&ruta), Err(Error::SchemaDesconocida { found: 999 })));
114 }
115}