Revert "chore(release): prepare v1.3.5"

This reverts commit ade146f9f3.
This commit is contained in:
Yuri Karamian
2026-08-17 17:03:01 +02:00
parent ade146f9f3
commit 43f15962f7
35 changed files with 3179 additions and 3978 deletions
+22 -60
View File
@@ -1,36 +1,22 @@
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use crate::types::VersionEntry;
fn safe_path_component<'a>(value: &'a str, label: &str) -> Result<&'a str, String> {
let mut components = Path::new(value).components();
if value.is_empty()
|| !matches!(components.next(), Some(Component::Normal(_)))
|| components.next().is_some()
{
return Err(format!("Invalid {label}"));
}
Ok(value)
}
/// Directory: .helixnotes/history/<note-id>/
fn history_dir(vault_path: &str, note_id: &str) -> Result<PathBuf, String> {
Ok(Path::new(vault_path)
fn history_dir(vault_path: &str, note_id: &str) -> PathBuf {
Path::new(vault_path)
.join(".helixnotes")
.join("history")
.join(safe_path_component(note_id, "note ID")?))
.join(note_id)
}
/// Save a version snapshot if enough time has passed since the last one.
/// Minimum interval: 5 minutes between snapshots.
pub fn maybe_snapshot(vault_path: &str, note_id: &str, raw_content: &str, max_versions: u32) {
let Ok(dir) = history_dir(vault_path, note_id) else {
log::warn!("Skipping history snapshot with an invalid note ID");
return;
};
let dir = history_dir(vault_path, note_id);
// Check if we should create a snapshot (5 min cooldown)
if let Ok(entries) = fs::read_dir(&dir) {
@@ -82,30 +68,31 @@ pub fn maybe_snapshot(vault_path: &str, note_id: &str, raw_content: &str, max_ve
}
/// Force-create a version snapshot, bypassing the cooldown.
pub fn force_snapshot(
vault_path: &str,
note_id: &str,
raw_content: &str,
max_versions: u32,
) -> Result<(), String> {
let dir = history_dir(vault_path, note_id)?;
fs::create_dir_all(&dir).map_err(|error| error.to_string())?;
pub fn force_snapshot(vault_path: &str, note_id: &str, raw_content: &str, max_versions: u32) {
let dir = history_dir(vault_path, note_id);
if let Err(e) = fs::create_dir_all(&dir) {
eprintln!("Failed to create history dir: {}", e);
return;
}
let timestamp = Utc::now().format("%Y-%m-%dT%H-%M-%S").to_string();
let filename = format!("{}.md", timestamp);
let path = dir.join(&filename);
fs::write(&path, raw_content).map_err(|error| error.to_string())?;
if let Err(e) = fs::write(&path, raw_content) {
eprintln!("Failed to write version snapshot: {}", e);
return;
}
if max_versions > 0 {
prune_versions(&dir, max_versions)?;
let _ = prune_versions(&dir, max_versions);
}
Ok(())
}
/// List all version snapshots for a note, newest first.
pub fn list_versions(vault_path: &str, note_id: &str) -> Result<Vec<VersionEntry>, String> {
let dir = history_dir(vault_path, note_id)?;
let dir = history_dir(vault_path, note_id);
if !dir.exists() {
return Ok(Vec::new());
}
@@ -115,7 +102,7 @@ pub fn list_versions(vault_path: &str, note_id: &str) -> Result<Vec<VersionEntry
for entry in fs::read_dir(&dir).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "md") {
if path.extension().map_or(false, |ext| ext == "md") {
let filename = path
.file_stem()
.unwrap_or_default()
@@ -147,18 +134,17 @@ pub fn list_versions(vault_path: &str, note_id: &str) -> Result<Vec<VersionEntry
/// Get the raw content of a specific version.
pub fn get_version(vault_path: &str, note_id: &str, timestamp: &str) -> Result<String, String> {
safe_path_component(timestamp, "version timestamp")?;
// Convert ISO timestamp back to filename: 2026-02-08T18:30:00Z → 2026-02-08T18-30-00.md
let filename = if let Some(t_pos) = timestamp.find('T') {
let date_part = &timestamp[..t_pos];
let time_part = timestamp[t_pos + 1..].trim_end_matches('Z');
let time_dashes = time_part.replace(':', "-");
format!("{date_part}T{time_dashes}.md")
format!("{}.md", format!("{}T{}", date_part, time_dashes))
} else {
format!("{}.md", timestamp)
};
let path = history_dir(vault_path, note_id)?.join(&filename);
let path = history_dir(vault_path, note_id).join(&filename);
fs::read_to_string(&path).map_err(|e| format!("Version not found: {}", e))
}
@@ -168,7 +154,7 @@ fn prune_versions(dir: &Path, max: u32) -> Result<(), String> {
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|ext| ext == "md"))
.filter(|p| p.extension().map_or(false, |ext| ext == "md"))
.collect();
// Sort by name (timestamps sort lexicographically) - newest last
@@ -183,27 +169,3 @@ fn prune_versions(dir: &Path, max: u32) -> Result<(), String> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::{get_version, list_versions};
use std::fs;
use uuid::Uuid;
#[test]
fn rejects_history_path_traversal() {
let vault =
std::env::temp_dir().join(format!("helixnotes-history-test-{}", Uuid::new_v4()));
let metadata = vault.join(".helixnotes");
let escaped_history = metadata.join("escaped");
fs::create_dir_all(&escaped_history).unwrap();
fs::write(escaped_history.join("2026-01-01T00-00-00.md"), "escaped").unwrap();
fs::create_dir_all(metadata.join("history").join("safe")).unwrap();
fs::write(metadata.join("secret.md"), "secret").unwrap();
assert!(list_versions(&vault.to_string_lossy(), "../escaped").is_err());
assert!(get_version(&vault.to_string_lossy(), "safe", "../../secret").is_err());
fs::remove_dir_all(vault).unwrap();
}
}