use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::models::note::Note;
use crate::storage::error::{StorageError, StorageResult};
use crate::storage::Storage;
#[derive(Debug, Serialize, Deserialize)]
struct NoteMetadata {
title: String,
created_at: DateTime<Local>,
updated_at: DateTime<Local>,
order: usize,
}
pub struct FSStorage {
root_dir: PathBuf,
}
impl Default for FSStorage {
fn default() -> Self {
Self::new()
}
}
impl FSStorage {
pub fn new() -> Self {
let home_dir = dirs::home_dir().expect("Failed to get home directory");
let root_dir = home_dir.join(".rnote");
Self { root_dir }
}
pub fn with_root_dir<P: AsRef<Path>>(root_dir: P) -> Self {
Self {
root_dir: root_dir.as_ref().to_path_buf(),
}
}
fn get_note_path(&self, title: &str) -> PathBuf {
let sanitized = title.replace("/", "_").replace("\\", "_");
self.root_dir.join(format!("{}.md", sanitized))
}
fn get_metadata_path(&self, title: &str) -> PathBuf {
let sanitized = title.replace("/", "_").replace("\\", "_");
self.root_dir.join(format!("{}.meta.json", sanitized))
}
fn read_metadata(&self, title: &str) -> StorageResult<NoteMetadata> {
let path = self.get_metadata_path(title);
let mut file =
File::open(&path).map_err(|_| StorageError::NoteNotFound(title.to_string()))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
serde_json::from_str(&contents).map_err(|e| StorageError::MetadataParse(e.to_string()))
}
fn write_metadata(&self, metadata: &NoteMetadata) -> StorageResult<()> {
let path = self.get_metadata_path(&metadata.title);
let temp_path = path.with_extension("meta.json.tmp");
let mut file = File::create(&temp_path)?;
let json = serde_json::to_string_pretty(metadata)
.map_err(|e| StorageError::MetadataParse(e.to_string()))?;
file.write_all(json.as_bytes())?;
file.flush()?;
fs::rename(temp_path, path)?;
Ok(())
}
}
impl Storage for FSStorage {
fn init(&self) -> StorageResult<()> {
if !self.root_dir.exists() {
fs::create_dir_all(&self.root_dir)
.map_err(|_| StorageError::DirectoryCreation(self.root_dir.clone()))?;
}
Ok(())
}
fn list_notes(&self) -> StorageResult<Vec<Note>> {
self.init()?;
let mut notes = Vec::new();
for entry in fs::read_dir(&self.root_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "md") {
if let Some(filename) = path.file_stem() {
let title = filename.to_string_lossy().to_string();
if let Ok(note) = self.read_note(&title) {
notes.push(note);
}
}
}
}
notes.sort_by_key(|note| note.order);
Ok(notes)
}
fn read_note(&self, title: &str) -> StorageResult<Note> {
let path = self.get_note_path(title);
let metadata_path = self.get_metadata_path(title);
if !path.exists() || !metadata_path.exists() {
return Err(StorageError::NoteNotFound(title.to_string()));
}
let mut file = File::open(&path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let metadata = self.read_metadata(title)?;
Ok(Note {
title: metadata.title,
content,
created_at: metadata.created_at,
updated_at: metadata.updated_at,
selected: false,
order: metadata.order,
})
}
fn write_note(&self, note: &Note) -> StorageResult<()> {
self.init()?;
let path = self.get_note_path(¬e.title);
let temp_path = path.with_extension("md.tmp");
let mut file = File::create(&temp_path)?;
file.write_all(note.content.as_bytes())?;
file.flush()?;
fs::rename(&temp_path, &path)?;
let metadata = NoteMetadata {
title: note.title.clone(),
created_at: note.created_at,
updated_at: note.updated_at,
order: note.order,
};
self.write_metadata(&metadata)?;
Ok(())
}
fn delete_note(&self, title: &str) -> StorageResult<()> {
let path = self.get_note_path(title);
let metadata_path = self.get_metadata_path(title);
if !path.exists() {
return Err(StorageError::NoteNotFound(title.to_string()));
}
if path.exists() {
fs::remove_file(path)?;
}
if metadata_path.exists() {
fs::remove_file(metadata_path)?;
}
Ok(())
}
}