From 42bed4ac1f43a75c6fbd8e8558eed98c5d60c17b Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 18:56:45 +0100 Subject: [PATCH 1/7] add full note backend API --- src-tauri/src/main.rs | 49 ++++++++++++++++++++++++++++++++--- src-tauri/src/note.rs | 52 ++++++++++++++++++++++++++++---------- src-tauri/src/noteerror.rs | 25 ++++++++++++++++++ 3 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/noteerror.rs diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1bc6387..6d20030 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,9 +3,11 @@ mod config; mod note; +mod noteerror; use config::{Config}; use note::{Note}; +use noteerror::{NoteResult}; fn main() { let config = Config::from_default_file(); @@ -14,7 +16,14 @@ fn main() { tauri::Builder::default() .manage(note) .invoke_handler(tauri::generate_handler![ - list + list, + read, + create, + rename, + write, + remove, + read_tags, + write_tags ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); @@ -24,7 +33,41 @@ fn main() { /// /// list all directories in the base directory that have a `README.md` file inside #[tauri::command] -async fn list(note: tauri::State<'_, Note>) -> std::result::Result, String> { - Ok(note.list()) +async fn list(note: tauri::State<'_, Note>) -> NoteResult> { + note.list() } +#[tauri::command] +async fn read(note: tauri::State<'_, Note>, name: &str) -> NoteResult { + note.read(name) +} + +#[tauri::command] +async fn create(note: tauri::State<'_, Note>) -> NoteResult { + note.create() +} + +#[tauri::command] +async fn rename(note: tauri::State<'_, Note>, old_name: &str, new_name: &str) -> NoteResult<()> { + note.rename(old_name, new_name) +} + +#[tauri::command] +async fn write(note: tauri::State<'_, Note>, name: &str, content: &str) -> NoteResult<()> { + note.write(name, content) +} + +#[tauri::command] +async fn remove(note: tauri::State<'_, Note>, name: &str) -> NoteResult<()> { + note.remove(name) +} + +#[tauri::command] +async fn read_tags(note: tauri::State<'_, Note>, name: &str) -> NoteResult> { + note.read_tags(name) +} + +#[tauri::command] +async fn write_tags(note: tauri::State<'_, Note>, name: &str, tags: Vec) -> NoteResult<()> { + note.write_tags(name, &tags) +} diff --git a/src-tauri/src/note.rs b/src-tauri/src/note.rs index dec5482..08111b2 100644 --- a/src-tauri/src/note.rs +++ b/src-tauri/src/note.rs @@ -1,5 +1,6 @@ -use std::{fs::DirEntry, io::{Error, Result}, path::{Path, PathBuf}}; +use std::{fs::DirEntry, path::{Path, PathBuf}}; use crate::config::{Config}; +use crate::noteerror::{NoteError, NoteResult}; pub struct Note { config: Config @@ -16,23 +17,51 @@ impl Note { /// list the names of the notes /// /// list all directories in the base directory that have a `README.md` file inside - pub fn list(&self) -> Vec { + pub fn list(&self) -> Result, NoteError> { let base_path = self.config.get_base_path().join(".notes"); match get_note_names(base_path) { - Ok(note_names) => return note_names, + Ok(note_names) => return Ok(note_names), Err(e) => error_handling(e.to_string()) } - Vec::::new() + Ok(Vec::::new()) } + pub fn read(&self, name : &str) -> NoteResult { + Ok("Dummy content".into()) + } + + pub fn create(&self) -> NoteResult { + Ok("New Note".into()) + } + + pub fn rename(&self, old_name: &str, new_name: &str) -> NoteResult<()> { + Err(NoteError::new("note implemented")) + } + + pub fn write(&self, name: &str, content: &str) -> NoteResult<()> { + Err(NoteError::new("note implemented")) + } + + pub fn remove(&self, name: &str) -> NoteResult<()> { + Err(NoteError::new("note implemented")) + } + + pub fn read_tags(&self, name: &str) -> NoteResult> { + let tags: Vec = Vec::new(); + Ok(tags) + } + + pub fn write_tags(&self, name: &str, tags: &Vec) -> NoteResult<()> { + Err(NoteError::new("note implemented")) + } } /// list all directories in `base_path` that have a `README.md` file inside -fn get_note_names(base_path: PathBuf) -> Result> { +fn get_note_names(base_path: PathBuf) -> NoteResult> { let mut note_names = Vec::::new(); let dir_entries = std::fs::read_dir(base_path)?; for dir_entry in dir_entries { - match get_note_name(dir_entry) { + match get_note_name(dir_entry?) { Ok(note_name) => note_names.push(note_name), Err(e) => error_handling(e.to_string()) } @@ -41,17 +70,12 @@ fn get_note_names(base_path: PathBuf) -> Result> { } /// check if the `dir_entry` contains a `README.md` file inside and returns the folder name if so -fn get_note_name(dir_entry: Result) -> Result { - let dir_entry = dir_entry?; +fn get_note_name(dir_entry: DirEntry) -> NoteResult { if dir_entry.file_type()?.is_dir() && Path::new(&dir_entry.path()).join("README.md").is_file() { return Ok(dir_entry.file_name().into_string().unwrap()); } - Err( - Error::new( - std::io::ErrorKind::NotFound, - format!("README.md not found for `{}`.", dir_entry.path().to_str().unwrap()) - ) - ) + + Err(NoteError::new(&format!("README.md not found for `{:?}`.", dir_entry.path()))) } fn error_handling(e: String) { diff --git a/src-tauri/src/noteerror.rs b/src-tauri/src/noteerror.rs new file mode 100644 index 0000000..0bdf6f9 --- /dev/null +++ b/src-tauri/src/noteerror.rs @@ -0,0 +1,25 @@ +use serde::{Serialize, Deserialize}; +use core::fmt::{Display}; + +#[derive(Serialize, Deserialize,Debug)] +pub struct NoteError { + message: String +} + +impl NoteError { + pub fn new(message: &str) -> Self { + NoteError { message: message.into() } + } + + pub fn to_string(&self) -> String { + self.message.clone() + } +} + +impl From for NoteError { + fn from(value: E) -> Self { + NoteError { message: value.to_string() } + } +} + +pub type NoteResult = Result; \ No newline at end of file From d52fe2e0d4c1c01d3cd9eba64afa68782301455d Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 18:57:22 +0100 Subject: [PATCH 2/7] use NoteError in config and remove ConfgError --- src-tauri/src/config.rs | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index bc27126..6348f86 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -3,7 +3,8 @@ use home::{home_dir}; use std::fs; use std::path::{Path, PathBuf}; -use core::fmt::Display; + +use crate::noteerror::{NoteError,NoteResult}; const DEFAULT_CONFIG_FILENAME: &str = ".noters.yaml"; @@ -32,23 +33,6 @@ fn get_home_dir() -> PathBuf { home_dir().unwrap_or(PathBuf::from(".")) } -#[derive(Debug)] -struct ConfigError { - message: String -} - -impl ConfigError { - fn new(message: &str) -> Self { - ConfigError { message: message.into() } - } -} - -impl From for ConfigError { - fn from(value: E) -> Self { - ConfigError { message: value.to_string() } - } -} - impl Config { /// Creates a new config with default values. @@ -83,7 +67,7 @@ impl Config { match Config::from_file(filename.as_path()) { Ok(config) => config, Err(error) => { - eprintln!("warning: failed to load config {:?}: {}", filename, error.message); + eprintln!("warning: failed to load config {:?}: {}", filename, error.to_string()); let config = Config::new(filename.as_path()); config.save(); config @@ -92,12 +76,12 @@ impl Config { } /// Try to load the config from a given path. - fn from_file(filename: &Path) -> Result { + fn from_file(filename: &Path) -> NoteResult { let text = fs::read_to_string(filename)?; let config_file = serde_yaml::from_str::(&text)?; if config_file.meta.version != 1 { - return Err(ConfigError::new("unknown file format (version mismatch)")); + return Err(NoteError::new("unknown file format (version mismatch)")); } Ok(Config { From bb1e94e8a142500445df5ec5b9cd1cd064bc3750 Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 18:57:50 +0100 Subject: [PATCH 3/7] use TauriNoteProvider in frontend --- frontend/main.js | 4 +-- frontend/taurinoteprovider.js | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 frontend/taurinoteprovider.js diff --git a/frontend/main.js b/frontend/main.js index ad29a55..5ded296 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -4,7 +4,7 @@ import "lineicons/web-font/lineicons.css" import { slider_attach } from "./slider.js" import { init_titlebar } from "./titlebar.js" import { init_settings } from "./settings.js" -import { FakeNoteProvider } from "./fakenoteprovider.js" +import { TauriNoteProvider } from "./taurinoteprovider.js" import { NoteList } from "./notelist.js" import { Editor } from "./editor.js" import { TagList } from "./taglist.js" @@ -30,7 +30,7 @@ document.querySelector("#toggle-mode").addEventListener("click", () => { }); -const noteProvider = new FakeNoteProvider(); +const noteProvider = new TauriNoteProvider(); const taglist_elemnt = document.querySelector("#taglist"); const taglist = new TagList(taglist_elemnt, noteProvider); diff --git a/frontend/taurinoteprovider.js b/frontend/taurinoteprovider.js new file mode 100644 index 0000000..8384454 --- /dev/null +++ b/frontend/taurinoteprovider.js @@ -0,0 +1,47 @@ +import { tauri } from "@tauri-apps/api"; +import { NoteProvider } from "./noteprovider"; + +class TauriNoteProvider extends NoteProvider { + async list() { + return await tauri.invoke("list"); + } + + async read(name) { + return await tauri.invoke("read", { name: name }); + } + + async create() { + return await tauri.invoke("create"); + } + + async rename(old_name, new_name) { + return await tauri.invoke("read", { + old_name: old_name, + new_name: new_name + }); + } + + async write(name, content) { + await tauri.invoke("write", { + name: name, + content: content + }); + } + + async remove(name) { + await tauri.invoke("remove", { name: name}); + } + + async read_tags(name) { + return await tauri.invoke("read_tags", { name: name}); + } + + async write_tags(name, tags) { + await tauri.invoke("write_tags", { + name: name, + tags: tags + }); + } +} + +export { TauriNoteProvider } \ No newline at end of file From ed406e6a8b5e583ef112ef96c47fe5695d75d732 Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 20:06:47 +0100 Subject: [PATCH 4/7] fix rename (note that tauri renames arguments to camel case) --- frontend/taurinoteprovider.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/taurinoteprovider.js b/frontend/taurinoteprovider.js index 8384454..8b63d8d 100644 --- a/frontend/taurinoteprovider.js +++ b/frontend/taurinoteprovider.js @@ -15,9 +15,9 @@ class TauriNoteProvider extends NoteProvider { } async rename(old_name, new_name) { - return await tauri.invoke("read", { - old_name: old_name, - new_name: new_name + return await tauri.invoke("rename", { + oldName: old_name, + newName: new_name }); } From df45ee43c9f688b67517f6a029ae8bc143c1ed7c Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 20:07:06 +0100 Subject: [PATCH 5/7] add backend implementation --- src-tauri/src/note.rs | 65 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/note.rs b/src-tauri/src/note.rs index 08111b2..b271c02 100644 --- a/src-tauri/src/note.rs +++ b/src-tauri/src/note.rs @@ -1,4 +1,4 @@ -use std::{fs::DirEntry, path::{Path, PathBuf}}; +use std::{fs, fs::DirEntry, path::{Path, PathBuf}}; use crate::config::{Config}; use crate::noteerror::{NoteError, NoteResult}; @@ -14,11 +14,11 @@ impl Note { } - /// list the names of the notes + /// Lists the names of the notes. /// /// list all directories in the base directory that have a `README.md` file inside pub fn list(&self) -> Result, NoteError> { - let base_path = self.config.get_base_path().join(".notes"); + let base_path = self.config.get_base_path(); match get_note_names(base_path) { Ok(note_names) => return Ok(note_names), Err(e) => error_handling(e.to_string()) @@ -26,33 +26,76 @@ impl Note { Ok(Vec::::new()) } + /// Reads the contents of the given note. pub fn read(&self, name : &str) -> NoteResult { - Ok("Dummy content".into()) + let readme = Path::new(&self.get_note_path(name)).join("README.md"); + Ok(fs::read_to_string(readme)?) } + /// Creates a new note and return it's name. pub fn create(&self) -> NoteResult { - Ok("New Note".into()) + let mut name = String::from("Untitled"); + let mut path = self.get_note_path(&name); + let mut n = 0; + while path.as_path().exists() { + n += 1; + name = format!("Untitled {}", n); + path = self.get_note_path(&name); + } + + fs::create_dir(path.clone())?; + let readme = Path::new(&path).join("README.md"); + fs::write(readme, format!("# {}\n", name))?; + let tags = Path::new(&path).join("tags.txt"); + fs::write(tags, "")?; + + Ok(name) } + /// Renames a note. pub fn rename(&self, old_name: &str, new_name: &str) -> NoteResult<()> { - Err(NoteError::new("note implemented")) + let old_path = self.get_note_path(old_name); + let new_path = self.get_note_path(new_name); + Ok(fs::rename(old_path, new_path)?) } + /// Writes the content of the given notes. pub fn write(&self, name: &str, content: &str) -> NoteResult<()> { - Err(NoteError::new("note implemented")) + let readme = Path::new(&self.get_note_path(name)).join("README.md"); + fs::write(readme, content.as_bytes())?; + Ok(()) } + /// Removes the given note. pub fn remove(&self, name: &str) -> NoteResult<()> { - Err(NoteError::new("note implemented")) + let path = self.get_note_path(name); + Ok(fs::remove_dir_all(path)?) } + /// Reads the tags associated with the given note. pub fn read_tags(&self, name: &str) -> NoteResult> { - let tags: Vec = Vec::new(); - Ok(tags) + let tags = Path::new(&self.get_note_path(name)).join("tags.txt"); + let content = fs::read_to_string(tags)?; + let tags: Vec = content + .lines() + .filter(|s| !s.is_empty()) + .map(String::from) + .collect(); + Ok (tags) } + /// Replaces the tags of tags of the given note. pub fn write_tags(&self, name: &str, tags: &Vec) -> NoteResult<()> { - Err(NoteError::new("note implemented")) + let content = tags.join("\n"); + let tags = Path::new(&self.get_note_path(name)).join("tags.txt"); + fs::write(tags, content.as_bytes())?; + Ok(()) + } + + fn get_note_path(&self, name: &str) -> PathBuf { + let mut path = self.config.get_base_path(); + path.push(name); + path } } From 58a5f706fb977ee1798880daba7a4c2240dce5fc Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Sun, 28 Jan 2024 20:07:22 +0100 Subject: [PATCH 6/7] remove unused code (FakeNoteProvider) --- frontend/fakenoteprovider.js | 88 ------------------------------------ 1 file changed, 88 deletions(-) delete mode 100644 frontend/fakenoteprovider.js diff --git a/frontend/fakenoteprovider.js b/frontend/fakenoteprovider.js deleted file mode 100644 index 959ceb7..0000000 --- a/frontend/fakenoteprovider.js +++ /dev/null @@ -1,88 +0,0 @@ -import { NoteProvider } from "./noteprovider"; - -class Note { - name; - content; - tags; - - constructor(name, content, tags) { - this.name = name; - this.content = content || ""; - this.tags = tags || []; - } -} - -class FakeNoteProvider extends NoteProvider { - - #notes; - - constructor() { - super(); - this.#notes = new Map([ - ["Sample", new Note("Sample", "# Sample Note\n", ["Sample"])] - ]); - } - - #get_note(name) { - const note = this.#notes.get(name); - if (!note) { - throw new Error(`unknown note \"${name}\"`); - } - return note; - } - - async list() { - return [...this.#notes.keys()]; - } - - async read(name) { - const note = this.#get_note(name); - return note.content; - } - - async create() { - let name = "Untitled"; - let count = 0; - while (this.#notes.has(name)) { - count++; - name = `Untitled ${count}`; - } - - this.#notes.set(name, new Note(name)); - return name; - } - - async rename(old_name, new_name) { - const note = this.#get_note(old_name); - if (this.#notes.has(new_name)) { - throw new Error(`failed to rename to an existing note \"${new_name}\"`); - } - - this.#notes.delete(old_name); - note.name = new_name; - this.#notes.set(note.name, note); - } - - async write(name, content) { - const note = this.#get_note(name); - note.content = content; - } - - async remove(name) { - if (this.#notes.has(name)) { - this.#notes.delete(name); - } - } - - async read_tags(name) { - const note = this.#get_note(name); - return note.tags; - } - - async write_tags(name, tags) { - const note = this.#get_note(name); - note.tags = tags; - } -} - -export { FakeNoteProvider } \ No newline at end of file From ce0c145ac0cfb2e479a5a5e3acc429ce5e4e64ca Mon Sep 17 00:00:00 2001 From: Falk Werner Date: Wed, 31 Jan 2024 17:47:38 +0100 Subject: [PATCH 7/7] verify note name --- src-tauri/src/note.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/note.rs b/src-tauri/src/note.rs index b271c02..c168a4f 100644 --- a/src-tauri/src/note.rs +++ b/src-tauri/src/note.rs @@ -28,19 +28,19 @@ impl Note { /// Reads the contents of the given note. pub fn read(&self, name : &str) -> NoteResult { - let readme = Path::new(&self.get_note_path(name)).join("README.md"); + let readme = Path::new(&self.get_note_path(name)?).join("README.md"); Ok(fs::read_to_string(readme)?) } /// Creates a new note and return it's name. pub fn create(&self) -> NoteResult { let mut name = String::from("Untitled"); - let mut path = self.get_note_path(&name); + let mut path = self.get_note_path(&name)?; let mut n = 0; while path.as_path().exists() { n += 1; name = format!("Untitled {}", n); - path = self.get_note_path(&name); + path = self.get_note_path(&name)?; } fs::create_dir(path.clone())?; @@ -54,27 +54,27 @@ impl Note { /// Renames a note. pub fn rename(&self, old_name: &str, new_name: &str) -> NoteResult<()> { - let old_path = self.get_note_path(old_name); - let new_path = self.get_note_path(new_name); + let old_path = self.get_note_path(old_name)?; + let new_path = self.get_note_path(new_name)?; Ok(fs::rename(old_path, new_path)?) } /// Writes the content of the given notes. pub fn write(&self, name: &str, content: &str) -> NoteResult<()> { - let readme = Path::new(&self.get_note_path(name)).join("README.md"); + let readme = Path::new(&self.get_note_path(name)?).join("README.md"); fs::write(readme, content.as_bytes())?; Ok(()) } /// Removes the given note. pub fn remove(&self, name: &str) -> NoteResult<()> { - let path = self.get_note_path(name); + let path = self.get_note_path(name)?; Ok(fs::remove_dir_all(path)?) } /// Reads the tags associated with the given note. pub fn read_tags(&self, name: &str) -> NoteResult> { - let tags = Path::new(&self.get_note_path(name)).join("tags.txt"); + let tags = Path::new(&self.get_note_path(name)?).join("tags.txt"); let content = fs::read_to_string(tags)?; let tags: Vec = content .lines() @@ -87,18 +87,27 @@ impl Note { /// Replaces the tags of tags of the given note. pub fn write_tags(&self, name: &str, tags: &Vec) -> NoteResult<()> { let content = tags.join("\n"); - let tags = Path::new(&self.get_note_path(name)).join("tags.txt"); + let tags = Path::new(&self.get_note_path(name)?).join("tags.txt"); fs::write(tags, content.as_bytes())?; Ok(()) } - fn get_note_path(&self, name: &str) -> PathBuf { + fn get_note_path(&self, name: &str) -> NoteResult { + check_name(name)?; let mut path = self.config.get_base_path(); path.push(name); - path + Ok(path) } } +/// Verifies, that a note name is valid +fn check_name(name: &str) -> NoteResult<()> { + match name.chars().nth(0) { + None | Some('/') | Some('\\') => Err(NoteError::new("invalid note name")), + _ => Ok(()) + } +} + /// list all directories in `base_path` that have a `README.md` file inside fn get_note_names(base_path: PathBuf) -> NoteResult> { let mut note_names = Vec::::new();