Remove owner, cadence, created_at, and created_time as hardcoded fields from the Frontmatter struct — they don't drive app logic and belong in generic properties. Owner and cadence values now flow through to the properties map (including single-element array unwrapping). Creation date is sourced from filesystem metadata (birthtime on macOS) instead of frontmatter. Also fixes title extraction: add H1 heading extraction to extract_title() (priority: frontmatter title → H1 → filename slug). Previously titles fell back directly to filename when no frontmatter title was set. StringOrList is retained for scalar Frontmatter fields as a defensive measure: YAML values can arrive as single-element arrays (e.g. Status: [Active]) which would cause entire deserialization to fail without it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
66 lines
2.3 KiB
Rust
66 lines
2.3 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
/// Read file metadata (modified_at timestamp, created_at timestamp, file size).
|
|
/// Creation time is sourced from filesystem metadata (birthtime on macOS).
|
|
pub(crate) fn read_file_metadata(path: &Path) -> Result<(Option<u64>, Option<u64>, u64), String> {
|
|
let metadata =
|
|
fs::metadata(path).map_err(|e| format!("Failed to stat {}: {}", path.display(), e))?;
|
|
let modified_at = metadata
|
|
.modified()
|
|
.ok()
|
|
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
|
.map(|d| d.as_secs());
|
|
let created_at = metadata
|
|
.created()
|
|
.ok()
|
|
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
|
.map(|d| d.as_secs());
|
|
Ok((modified_at, created_at, metadata.len()))
|
|
}
|
|
|
|
/// Read the content of a single note file.
|
|
pub fn get_note_content(path: &Path) -> Result<String, String> {
|
|
if !path.exists() {
|
|
return Err(format!("File does not exist: {}", path.display()));
|
|
}
|
|
if !path.is_file() {
|
|
return Err(format!("Path is not a file: {}", path.display()));
|
|
}
|
|
fs::read_to_string(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))
|
|
}
|
|
|
|
fn validate_save_path(file_path: &Path, display_path: &str) -> Result<(), String> {
|
|
let parent_missing = file_path.parent().is_some_and(|p| !p.exists());
|
|
if parent_missing {
|
|
return Err(format!(
|
|
"Parent directory does not exist: {}",
|
|
file_path.parent().unwrap().display()
|
|
));
|
|
}
|
|
let is_readonly = file_path.exists()
|
|
&& file_path
|
|
.metadata()
|
|
.map(|m| m.permissions().readonly())
|
|
.unwrap_or(false);
|
|
if is_readonly {
|
|
return Err(format!("File is read-only: {}", display_path));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Write content to a note file. Creates parent directory if needed, validates path,
|
|
/// then writes content to disk.
|
|
pub fn save_note_content(path: &str, content: &str) -> Result<(), String> {
|
|
let file_path = Path::new(path);
|
|
if let Some(parent) = file_path.parent() {
|
|
if !parent.exists() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
|
|
}
|
|
}
|
|
validate_save_path(file_path, path)?;
|
|
fs::write(file_path, content).map_err(|e| format!("Failed to save {}: {}", path, e))
|
|
}
|