mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-20 18:07:29 +02:00
Revert "chore(release): prepare v1.3.5"
This reverts commit ade146f9f3.
This commit is contained in:
+22
-60
@@ -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 = ×tamp[..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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user