feat(vault): seal transcript/notes/summary at rest via storage read/write helpers (T8.8)
This commit is contained in:
1 parent
f12e86513a
commit
0f7af4576e
1 file changed
+40
-20
@@ -477,21 +477,19 @@ impl SqliteStore {
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
let transcript_text =
|
||||
std::fs::read_to_string(paths::meeting_dir(id).join("transcript.json"))
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
|
||||
.map(|t| {
|
||||
t.segments
|
||||
.iter()
|
||||
.map(|s| s.text.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let transcript_text = read_artifact(&paths::meeting_dir(id).join("transcript.json"))
|
||||
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
|
||||
.map(|t| {
|
||||
t.segments
|
||||
.iter()
|
||||
.map(|s| s.text.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let notes_text =
|
||||
std::fs::read_to_string(paths::meeting_dir(id).join("notes.md")).unwrap_or_default();
|
||||
read_artifact(&paths::meeting_dir(id).join("notes.md")).unwrap_or_default();
|
||||
|
||||
sqlx::query("DELETE FROM meeting_fts WHERE meeting_id = ?")
|
||||
.bind(id)
|
||||
@@ -664,7 +662,10 @@ impl Store for SqliteStore {
|
||||
};
|
||||
let json =
|
||||
serde_json::to_string_pretty(&transcript).map_err(|e| StoreError::Db(e.to_string()))?;
|
||||
std::fs::write(paths::meeting_dir(id).join("transcript.json"), json)?;
|
||||
write_artifact(
|
||||
&paths::meeting_dir(id).join("transcript.json"),
|
||||
json.as_bytes(),
|
||||
)?;
|
||||
self.reindex_fts(id).await?;
|
||||
let template_id: Option<String> =
|
||||
sqlx::query_scalar("SELECT template_id FROM meetings WHERE id = ?")
|
||||
@@ -778,8 +779,7 @@ impl Store for SqliteStore {
|
||||
.collect();
|
||||
|
||||
let folder = paths::meeting_dir(id);
|
||||
let mut segments = std::fs::read_to_string(folder.join("transcript.json"))
|
||||
.ok()
|
||||
let mut segments = read_artifact(&folder.join("transcript.json"))
|
||||
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
|
||||
.map(|t| t.segments)
|
||||
.unwrap_or_default();
|
||||
@@ -791,9 +791,8 @@ impl Store for SqliteStore {
|
||||
seg.speaker = resolve_canonical(&seg.speaker, &merge_map).to_string();
|
||||
}
|
||||
}
|
||||
let notes_markdown = std::fs::read_to_string(folder.join("notes.md")).unwrap_or_default();
|
||||
let summary = std::fs::read_to_string(folder.join("summary.json"))
|
||||
.ok()
|
||||
let notes_markdown = read_artifact(&folder.join("notes.md")).unwrap_or_default();
|
||||
let summary = read_artifact(&folder.join("summary.json"))
|
||||
.and_then(|s| serde_json::from_str::<SummaryFile>(&s).ok());
|
||||
let tags = self.tags_for_meeting(id).await?;
|
||||
|
||||
@@ -837,7 +836,10 @@ impl Store for SqliteStore {
|
||||
}
|
||||
|
||||
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError> {
|
||||
std::fs::write(paths::meeting_dir(id).join("notes.md"), markdown)?;
|
||||
write_artifact(
|
||||
&paths::meeting_dir(id).join("notes.md"),
|
||||
markdown.as_bytes(),
|
||||
)?;
|
||||
sqlx::query("UPDATE meetings SET updated_at = ? WHERE id = ?")
|
||||
.bind(now_unix())
|
||||
.bind(id)
|
||||
@@ -1450,6 +1452,24 @@ impl Store for SqliteStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a derived artifact, transparently decrypting it if the vault sealed it
|
||||
/// (T8.8, FR-SEC-3). Returns `None` if the file is missing or can't be
|
||||
/// decrypted (vault locked / wrong key) — callers already treat a missing
|
||||
/// artifact as empty, so a locked vault degrades to "no content" rather than a
|
||||
/// crash.
|
||||
fn read_artifact(path: &std::path::Path) -> Option<String> {
|
||||
let bytes = std::fs::read(path).ok()?;
|
||||
let plain = crate::vault::open(&bytes).ok()?;
|
||||
Some(String::from_utf8_lossy(&plain).into_owned())
|
||||
}
|
||||
|
||||
/// Write a derived artifact, sealing it when the vault is unlocked (a no-op
|
||||
/// passthrough otherwise). Audio is not sealed here — it's handled separately.
|
||||
fn write_artifact(path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
|
||||
let sealed = crate::vault::seal(contents).map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
std::fs::write(path, sealed)
|
||||
}
|
||||
|
||||
fn dir_size(path: &str) -> u64 {
|
||||
std::fs::read_dir(path)
|
||||
.into_iter()
|
||||
|
||||
Reference in new issue
Block a user