diff --git a/rslib/backend.proto b/rslib/backend.proto index 9ed4e824d..a362810d8 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -88,6 +88,9 @@ enum ServiceIndex { SERVICE_INDEX_CARD_RENDERING = 6; SERVICE_INDEX_DECK_CONFIG = 7; SERVICE_INDEX_TAGS = 8; + SERVICE_INDEX_SEARCH = 9; + SERVICE_INDEX_STATS = 10; + SERVICE_INDEX_MEDIA = 11; SERVICE_INDEX_BACKEND = 99; } @@ -209,33 +212,33 @@ service TagsService { rpc DragDropTags(DragDropTagsIn) returns (Empty); } -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // searching - +service SearchService { rpc BuildSearchString(SearchNode) returns (String); rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); +} - // stats - +service StatsService { rpc CardStats(CardID) returns (String); rpc Graphs(GraphsIn) returns (GraphsOut); rpc GetGraphPreferences(Empty) returns (GraphPreferences); rpc SetGraphPreferences(GraphPreferences) returns (Empty); +} - // media - +service MediaService { rpc CheckMedia(Empty) returns (CheckMediaOut); rpc TrashMediaFiles(TrashMediaFilesIn) returns (Empty); rpc AddMediaFile(AddMediaFileIn) returns (String); rpc EmptyTrash(Empty) returns (Empty); rpc RestoreTrash(Empty) returns (Empty); +} + +service BackendService { + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); // cards diff --git a/rslib/src/backend/media.rs b/rslib/src/backend/media.rs new file mode 100644 index 000000000..f36a92c60 --- /dev/null +++ b/rslib/src/backend/media.rs @@ -0,0 +1,89 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{progress::Progress, Backend}; +use crate::{ + backend_proto as pb, + media::{check::MediaChecker, MediaManager}, + prelude::*, +}; +pub(super) use pb::media_service::Service as MediaService; + +impl MediaService for Backend { + // media + //----------------------------------------------- + + fn check_media(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + let mut output = checker.check()?; + + let report = checker.summarize_output(&mut output); + + Ok(pb::CheckMediaOut { + unused: output.unused, + missing: output.missing, + report, + have_trash: output.trash_count > 0, + }) + }) + }) + } + + fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + let mut ctx = mgr.dbctx(); + mgr.remove_files(&mut ctx, &input.fnames) + }) + .map(Into::into) + } + + fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + let mut ctx = mgr.dbctx(); + Ok(mgr + .add_file(&mut ctx, &input.desired_name, &input.data)? + .to_string() + .into()) + }) + } + + fn empty_trash(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + + checker.empty_trash() + }) + }) + .map(Into::into) + } + + fn restore_trash(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + + checker.restore_trash() + }) + }) + .map(Into::into) + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 91b591662..bd48cb2bb 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -10,11 +10,13 @@ mod deckconfig; mod decks; mod err; mod generic; +mod media; mod notes; mod notetypes; mod progress; mod scheduler; mod search; +mod stats; mod sync; mod tags; @@ -23,9 +25,12 @@ use self::{ config::ConfigService, deckconfig::DeckConfigService, decks::DecksService, + media::MediaService, notes::NotesService, notetypes::NoteTypesService, scheduler::SchedulingService, + search::SearchService, + stats::StatsService, sync::{SyncService, SyncState}, tags::TagsService, }; @@ -42,12 +47,8 @@ use crate::{ log, log::default_logger, markdown::render_markdown, - media::check::MediaChecker, - media::MediaManager, - notes::NoteID, notetype::RenderCardOutput, scheduler::timespan::{answer_button_time, time_span}, - search::{concatenate_searches, replace_search_node, write_nodes, Node}, template::RenderedNode, text::sanitize_html_no_images, undo::UndoableOpKind, @@ -112,175 +113,6 @@ impl BackendService for Backend { Ok(().into()) } - // searching - //----------------------------------------------- - - fn build_search_string(&self, input: pb::SearchNode) -> Result { - let node: Node = input.try_into()?; - Ok(write_nodes(&node.into_node_list()).into()) - } - - fn search_cards(&self, input: pb::SearchCardsIn) -> Result { - self.with_col(|col| { - let order = input.order.unwrap_or_default().value.into(); - let cids = col.search_cards(&input.search, order)?; - Ok(pb::SearchCardsOut { - card_ids: cids.into_iter().map(|v| v.0).collect(), - }) - }) - } - - fn search_notes(&self, input: pb::SearchNotesIn) -> Result { - self.with_col(|col| { - let nids = col.search_notes(&input.search)?; - Ok(pb::SearchNotesOut { - note_ids: nids.into_iter().map(|v| v.0).collect(), - }) - }) - } - - fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result { - let sep = input.joiner().into(); - let existing_nodes = { - let node: Node = input.existing_node.unwrap_or_default().try_into()?; - node.into_node_list() - }; - let additional_node = input.additional_node.unwrap_or_default().try_into()?; - Ok(concatenate_searches(sep, existing_nodes, additional_node).into()) - } - - fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result { - let existing = { - let node = input.existing_node.unwrap_or_default().try_into()?; - if let Node::Group(nodes) = node { - nodes - } else { - vec![node] - } - }; - let replacement = input.replacement_node.unwrap_or_default().try_into()?; - Ok(replace_search_node(existing, replacement).into()) - } - - fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { - let mut search = if input.regex { - input.search - } else { - regex::escape(&input.search) - }; - if !input.match_case { - search = format!("(?i){}", search); - } - let nids = input.nids.into_iter().map(NoteID).collect(); - let field_name = if input.field_name.is_empty() { - None - } else { - Some(input.field_name) - }; - let repl = input.replacement; - self.with_col(|col| { - col.find_and_replace(nids, &search, &repl, field_name) - .map(|cnt| pb::UInt32 { val: cnt as u32 }) - }) - } - // statistics - //----------------------------------------------- - - fn card_stats(&self, input: pb::CardId) -> Result { - self.with_col(|col| col.card_stats(input.into())) - .map(Into::into) - } - - fn graphs(&self, input: pb::GraphsIn) -> Result { - self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) - } - - fn get_graph_preferences(&self, _input: pb::Empty) -> Result { - self.with_col(|col| col.get_graph_preferences()) - } - - fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { - self.with_col(|col| col.set_graph_preferences(input)) - .map(Into::into) - } - - // media - //----------------------------------------------- - - fn check_media(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - let mut output = checker.check()?; - - let report = checker.summarize_output(&mut output); - - Ok(pb::CheckMediaOut { - unused: output.unused, - missing: output.missing, - report, - have_trash: output.trash_count > 0, - }) - }) - }) - } - - fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - let mut ctx = mgr.dbctx(); - mgr.remove_files(&mut ctx, &input.fnames) - }) - .map(Into::into) - } - - fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - let mut ctx = mgr.dbctx(); - Ok(mgr - .add_file(&mut ctx, &input.desired_name, &input.data)? - .to_string() - .into()) - }) - } - - fn empty_trash(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - - checker.empty_trash() - }) - }) - .map(Into::into) - } - - fn restore_trash(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - - checker.restore_trash() - }) - }) - .map(Into::into) - } - // cards //------------------------------------------------------------------- @@ -494,6 +326,9 @@ impl Backend { pb::ServiceIndex::CardRendering => { CardRenderingService::run_method(self, method, input) } + pb::ServiceIndex::Media => MediaService::run_method(self, method, input), + pb::ServiceIndex::Stats => StatsService::run_method(self, method, input), + pb::ServiceIndex::Search => SearchService::run_method(self, method, input), }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); diff --git a/rslib/src/backend/search.rs b/rslib/src/backend/search.rs index d0b134c2a..6bda494ac 100644 --- a/rslib/src/backend/search.rs +++ b/rslib/src/backend/search.rs @@ -4,6 +4,7 @@ use itertools::Itertools; use std::convert::{TryFrom, TryInto}; +use super::Backend; use crate::{ backend_proto as pb, backend_proto::{ @@ -12,11 +13,83 @@ use crate::{ config::SortKind, prelude::*, search::{ - parse_search, BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, - StateKind, TemplateKind, + concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, + PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, }, text::escape_anki_wildcards, }; +pub(super) use pb::search_service::Service as SearchService; + +impl SearchService for Backend { + fn build_search_string(&self, input: pb::SearchNode) -> Result { + let node: Node = input.try_into()?; + Ok(write_nodes(&node.into_node_list()).into()) + } + + fn search_cards(&self, input: pb::SearchCardsIn) -> Result { + self.with_col(|col| { + let order = input.order.unwrap_or_default().value.into(); + let cids = col.search_cards(&input.search, order)?; + Ok(pb::SearchCardsOut { + card_ids: cids.into_iter().map(|v| v.0).collect(), + }) + }) + } + + fn search_notes(&self, input: pb::SearchNotesIn) -> Result { + self.with_col(|col| { + let nids = col.search_notes(&input.search)?; + Ok(pb::SearchNotesOut { + note_ids: nids.into_iter().map(|v| v.0).collect(), + }) + }) + } + + fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result { + let sep = input.joiner().into(); + let existing_nodes = { + let node: Node = input.existing_node.unwrap_or_default().try_into()?; + node.into_node_list() + }; + let additional_node = input.additional_node.unwrap_or_default().try_into()?; + Ok(concatenate_searches(sep, existing_nodes, additional_node).into()) + } + + fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result { + let existing = { + let node = input.existing_node.unwrap_or_default().try_into()?; + if let Node::Group(nodes) = node { + nodes + } else { + vec![node] + } + }; + let replacement = input.replacement_node.unwrap_or_default().try_into()?; + Ok(replace_search_node(existing, replacement).into()) + } + + fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { + let mut search = if input.regex { + input.search + } else { + regex::escape(&input.search) + }; + if !input.match_case { + search = format!("(?i){}", search); + } + let nids = input.nids.into_iter().map(NoteID).collect(); + let field_name = if input.field_name.is_empty() { + None + } else { + Some(input.field_name) + }; + let repl = input.replacement; + self.with_col(|col| { + col.find_and_replace(nids, &search, &repl, field_name) + .map(|cnt| pb::UInt32 { val: cnt as u32 }) + }) + } +} impl TryFrom for Node { type Error = AnkiError; diff --git a/rslib/src/backend/stats.rs b/rslib/src/backend/stats.rs new file mode 100644 index 000000000..09c4f3f0a --- /dev/null +++ b/rslib/src/backend/stats.rs @@ -0,0 +1,26 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{backend_proto as pb, prelude::*}; +pub(super) use pb::stats_service::Service as StatsService; + +impl StatsService for Backend { + fn card_stats(&self, input: pb::CardId) -> Result { + self.with_col(|col| col.card_stats(input.into())) + .map(Into::into) + } + + fn graphs(&self, input: pb::GraphsIn) -> Result { + self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) + } + + fn get_graph_preferences(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.get_graph_preferences()) + } + + fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { + self.with_col(|col| col.set_graph_preferences(input)) + .map(Into::into) + } +}