From: Olaf Wintermann Date: Sat, 8 Aug 2026 11:59:40 +0000 (+0200) Subject: implement creating notes from files X-Git-Url: https://uap-core.de/gitweb/?a=commitdiff_plain;h=HEAD;p=note.git implement creating notes from files --- diff --git a/application/backend/src/backend.rs b/application/backend/src/backend.rs index 551643e..39fd72d 100644 --- a/application/backend/src/backend.rs +++ b/application/backend/src/backend.rs @@ -25,9 +25,10 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ +use std::ffi::OsStr; use std::fs::Metadata; use std::future::Future; -use std::io::Error; +use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; use std::pin::Pin; use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, Set, QueryOrder, DbErr, ExprTrait, QuerySelect}; @@ -44,7 +45,7 @@ use migration::{Expr, Migrator, MigratorTrait}; use entity::{collection, note, notecontent, profile, repository}; use entity::profile::Entity as Profile; use entity::collection::{create_notebook_hierarchy, CollectionType, Entity as Collection, Node}; -use entity::note::{Column, Entity as Note}; +use entity::note::{Column, Entity as Note, NoteType}; use entity::notecontent::{Entity as NoteContent}; use migration::prelude::Utc; use crate::lockmanager::LockManager; @@ -779,6 +780,86 @@ impl BackendHandle { let _ = self.tx.send(cmd); } + pub fn add_file_note(&self, collection_id: i32, path: &Path, callback: F) + where F: FnOnce(Result) + Send + 'static { + let bhandle = self.clone(); + let pathbuf = path.to_path_buf(); + let cmd = Box::pin(async move { + let name = pathbuf.as_path().file_name(); + // check file and get the file name + let result: Result<&OsStr, Error> = async { + let metadata = fs::metadata(pathbuf.as_path()).await?; + if !metadata.is_file() { + let msg = format!("{} is not a valid file", pathbuf.display()); + return Err(Error::new(ErrorKind::NotFound, msg)); + } + let name = pathbuf.file_name(); + if let Some(name) = name { + Ok(name) + } else { + // this shouldn't happen because the path points to a file + Err(Error::new(ErrorKind::NotFound, "no file name".to_string())) + } + }.await; + + let name = match result { + Ok(name) => { + let name = name.to_string_lossy().to_string(); + if name.len() > 0 { + name + } else { + // in case to_string_lossy removes everything + "new_file".to_string() + } + }, + Err(e) => { + callback(Err(DbErr::Custom(e.to_string()))); + return; + } + }; + + let result: Result = async { + // prepare db insert + let nodename = generate_nodename_in_collection(&bhandle.backend.db, collection_id, name.as_str()).await?; + let title = nodename.clone(); // TODO + let contenttype = "image/png".to_string(); // TODO + let insert = note::ActiveModel { + collection_id: Set(collection_id), + nodename: Set(nodename), + kind: Set(NoteType::File), + title: Set(title), + content_type: Set(contenttype), + lastmodified: Set(Utc::now().into()), + created: Set(Utc::now().into()), + fixed_title: Set(true), + version: Set(0), + + ..Default::default() + }; + // do insert + let model = insert.insert(&bhandle.backend.db).await?; + + // if local storage is enabled, copy the note to the note path + let note_local_path = get_note_storage_path(&bhandle.backend.db, model.note_id).await?; + if let Some(note_path) = note_local_path { + // copy file to new note path + let to_path = Path::new(¬e_path); + let copy_result = fs::copy(pathbuf.as_path(), to_path).await; + if let Err(e) = copy_result { + return Err(DbErr::Custom(e.to_string())) + } + } else { + // copy note content to the database + // TODO + } + + Ok(model) + }.await; + callback(result); + }); + let _ = self.tx.send(cmd); + } + pub fn get_file_metadata(&self, path: &Path, callback: F) where F: FnOnce(std::io::Result) + Send + 'static { let p = PathBuf::from(path); diff --git a/application/backend/src/storage.rs b/application/backend/src/storage.rs index 59694a7..7ab7178 100644 --- a/application/backend/src/storage.rs +++ b/application/backend/src/storage.rs @@ -38,6 +38,7 @@ use entity::{collection, filecontent, note, repository}; use entity::collection::LocalStorageSetting; use entity::filecontent::Model as FileContent; use migration::JoinType; +use crate::note::randomize_nodename; /// Checks if a collection should use local storage fn uses_local_storage(settings: LocalStorageSetting, repo_use_local: bool) -> bool { @@ -170,4 +171,40 @@ pub async fn check_free_nodename(db: &DatabaseConnection, note_id: i32, nodename } Ok(true) +} + +pub async fn generate_nodename_in_collection(db: &DatabaseConnection, collection_id: i32, nodename: &str) -> Result { + let exists = note::Entity::find() + .filter(note::Column::CollectionId.eq(collection_id)) + .filter(note::Column::Nodename.eq(nodename)) + .select_only() + .column(note::Column::NoteId) + .into_tuple::() + .one(db).await?; + + let collection_storage_path = get_collection_storage_path(db, collection_id).await?; + let mut local_exists = false; + if let Some(collection_path) = collection_storage_path { + let path = Path::new(&collection_path); + let node_path = path.join(nodename); + let metadata = fs::metadata(node_path).await; + match metadata { + Ok(metadata) => { + local_exists = true; + }, + Err(err) if err.kind() == ErrorKind::NotFound => { + // not an error, not found means the nodename is free + }, + Err(err) => { + return Err(DbErr::Custom(err.to_string())); + } + } + } + + if exists.is_none() && !local_exists { + Ok(nodename.to_string()) + } else { + // in theory the randomized nodename could exists, however it is very unlikely + Ok(randomize_nodename(nodename)) + } } \ No newline at end of file diff --git a/application/note/src/notebook.rs b/application/note/src/notebook.rs index 14712cb..e83bd18 100644 --- a/application/note/src/notebook.rs +++ b/application/note/src/notebook.rs @@ -297,20 +297,50 @@ impl Notebook { #[action] pub fn new_file_note(&mut self, event: &ActionEvent) { - println!("new file note"); - let Some(doc) = self.doc_ref.get_doc() else { return; }; - let proxy = doc.doc_proxy(); if let Some(obj) = &event.obj { + // TODO: toolkit feature request: it would be really nice, if I could specify + // an openfile_dialog action, which is called on the parent (toolkit issue #965) obj.openfile_dialog(false, move|event|{ - + if let EventType::FileList(files) = event.event_type && files.len() == 1 { + let file = files[0].clone(); + doc.ctx.call_action_with_parameter("add_file_note", Box::new(file)); + } }); } } + #[action(String)] + pub fn add_file_note(&mut self, _event: &ActionEvent, path: &String) { + println!("add_file_note: {}", path); + + let Some(doc) = self.doc_ref.get_doc() else { + return; + }; + let proxy = doc.doc_proxy(); + + let path = Path::new(path); + + self.backend.add_file_note(self.collection_id, path, |result|{ + proxy.call_mainthread(|_doc, nb|{ + match result { + Ok(model) => { + let item = NoteItem::from_model(model); + nb.notes.data_mut().insert(0, item); + nb.notes.update(); + nb.notes.select_with_event(0, true); + }, + Err(e) => { + eprintln!("add_file_note failed: {:?}", e); + } + } + }); + }); + } + #[action(NavigationItem)] pub fn navigate_to_note(&mut self, _event: &mut ActionEvent, nav: &NavigationItem) { //println!("navigate to note");