From 3f167f6fae37a371c8cca188443cc89db44f0ab2 Mon Sep 17 00:00:00 2001 From: David Bliss Date: Sun, 7 Apr 2024 16:55:30 +0100 Subject: [PATCH] Add database cleanup background task --- core/src/repo.rs | 14 ++++- src/app.rs | 79 ++++++++++++++++++++++++---- src/app/background/cleanup.rs | 98 +++++++++++++++++++++++++++++++++++ src/app/background/mod.rs | 1 + 4 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 src/app/background/cleanup.rs diff --git a/core/src/repo.rs b/core/src/repo.rs index 5bdc8169..06c03d30 100644 --- a/core/src/repo.rs +++ b/core/src/repo.rs @@ -142,7 +142,7 @@ impl Repository { preview_id INTEGER PRIMARY KEY UNIQUE NOT NULL, -- pk for preview picture_id INTEGER UNIQUE NOT NULL ON CONFLICT IGNORE, -- fk to pictures. Only one preview allowed for now. full_path TEXT UNIQUE NOT NULL ON CONFLICT IGNORE, -- full path to preview image - FOREIGN KEY (picture_id) REFERENCES pictures (picture_id) + FOREIGN KEY (picture_id) REFERENCES pictures (picture_id) ON DELETE CASCADE )", ]; @@ -324,6 +324,18 @@ impl Repository { let head = iter.flatten().nth(0); Ok(head) } + + pub fn remove(&mut self, picture_id: PictureId) -> Result<()> { + let mut stmt = self + .con + .prepare("DELETE FROM pictures WHERE picture_id = ?1") + .map_err(|e| RepositoryError(e.to_string()))?; + + stmt.execute([picture_id.0]) + .map_err(|e| RepositoryError(e.to_string()))?; + + Ok(()) + } } #[cfg(test)] diff --git a/src/app.rs b/src/app.rs index 570d7497..f7dbdafe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -57,17 +57,16 @@ use self::components::{ mod background; use self::background::{ - scan_photos::ScanPhotos, - scan_photos::ScanPhotosInput, - scan_photos::ScanPhotosOutput, - generate_previews::GeneratePreviews, - generate_previews::GeneratePreviewsInput, - generate_previews::GeneratePreviewsOutput, + scan_photos::{ScanPhotos, ScanPhotosInput, ScanPhotosOutput}, + generate_previews::{GeneratePreviews, GeneratePreviewsInput, GeneratePreviewsOutput}, + cleanup::{Cleanup, CleanupInput, CleanupOutput}, }; pub(super) struct App { scan_photos: WorkerController, generate_previews: WorkerController, + cleanup: WorkerController, + about_dialog: Controller, all_photos: AsyncController, month_photos: AsyncController, @@ -149,6 +148,11 @@ pub(super) enum AppMsg { ThumbnailGenerationStarted(usize), ThumbnailGenerated, ThumbnailGenerationCompleted, + + // Cleanup events + CleanupStarted(usize), + CleanupCleaned, + CleanupCompleted, } relm4::new_action_group!(pub(super) WindowActionGroup, "win"); @@ -429,6 +433,15 @@ impl SimpleComponent for App { GeneratePreviewsOutput::Completed => AppMsg::ThumbnailGenerationCompleted, }); + + let cleanup = Cleanup::builder() + .detach_worker(repo.clone()) + .forward(sender.input_sender(), |msg| match msg { + CleanupOutput::Started(count) => AppMsg::CleanupStarted(count), + CleanupOutput::Cleaned => AppMsg::CleanupCleaned, + CleanupOutput::Completed => AppMsg::CleanupCompleted, + }); + let all_photos = Album::builder() .launch((repo.clone(), AlbumFilter::All)) .forward(sender.input_sender(), |msg| match msg { @@ -502,6 +515,7 @@ impl SimpleComponent for App { let model = Self { scan_photos, generate_previews, + cleanup, about_dialog, all_photos, month_photos, @@ -550,6 +564,7 @@ impl SimpleComponent for App { model.all_photos.emit(AlbumInput::Refresh); model.scan_photos.sender().emit(ScanPhotosInput::Start); + // model.selfie_photos.emit(SelfiePhotosInput::Refresh); // model.month_photos.emit(MonthPhotosInput::Refresh); // model.year_photos.emit(YearPhotosInput::Refresh); @@ -633,7 +648,8 @@ impl SimpleComponent for App { self.month_photos.emit(MonthPhotosInput::Refresh); self.year_photos.emit(YearPhotosInput::Refresh); - self.generate_previews.emit(GeneratePreviewsInput::Start); + //self.generate_previews.emit(GeneratePreviewsInput::Start); + self.cleanup.emit(CleanupInput::Start); }, AppMsg::ThumbnailGenerationStarted(count) => { println!("Thumbnail generation started."); @@ -658,7 +674,7 @@ impl SimpleComponent for App { AppMsg::ThumbnailGenerated => { println!("Thumbnail generated."); self.progress_current_count += 1; - // Show pulsing for first 20 thumbnails so that it catches the eye, then + // Show pulsing for first 20 items so that it catches the eye, then // switch to fractional view if self.progress_current_count < 20 { self.progress_bar.pulse(); @@ -670,7 +686,52 @@ impl SimpleComponent for App { self.progress_bar.set_fraction(fraction); } }, - AppMsg::ThumbnailGenerationCompleted => { + AppMsg::CleanupCompleted => { + println!("Cleanup completed."); + self.spinner.stop(); + self.banner.set_revealed(false); + self.banner.set_button_label(None); + self.progress_box.set_visible(false); + + self.generate_previews.emit(GeneratePreviewsInput::Start); + }, + + AppMsg::CleanupStarted(count) => { + println!("Cleanup started."); + self.banner.set_title("Database maintenance."); + // Show button to refresh all photo grids. + //self.banner.set_button_label(Some("Refresh")); + self.banner.set_revealed(true); + + self.spinner.start(); + + let show = self.main_navigation.shows_sidebar(); + self.spinner.set_visible(!show); + + self.progress_end_count = count; + self.progress_current_count = 0; + + self.progress_box.set_visible(true); + self.progress_bar.set_fraction(0.0); + self.progress_bar.set_text(Some("Database maintenance")); + self.progress_bar.set_pulse_step(0.25); + }, + AppMsg::CleanupCleaned => { + println!("Cleanup cleaned."); + self.progress_current_count += 1; + // Show pulsing for first 1000 items so that it catches the eye, then + // switch to fractional view + if self.progress_current_count < 1000 { + self.progress_bar.pulse(); + } else { + if self.progress_current_count == 1000 { + self.progress_bar.set_text(None); + } + let fraction = self.progress_current_count as f64 / self.progress_end_count as f64; + self.progress_bar.set_fraction(fraction); + } + }, + AppMsg::ThumbnailGenerationCompleted => { println!("Thumbnail generation completed."); self.spinner.stop(); self.banner.set_revealed(false); diff --git a/src/app/background/cleanup.rs b/src/app/background/cleanup.rs new file mode 100644 index 00000000..a6710da3 --- /dev/null +++ b/src/app/background/cleanup.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: © 2024 David Bliss +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use relm4::prelude::*; +use relm4::Worker; +use std::sync::{Arc, Mutex}; +use photos_core::Result; +use rayon::prelude::*; + +#[derive(Debug)] +pub enum CleanupInput { + Start, +} + +#[derive(Debug)] +pub enum CleanupOutput { + // Thumbnail generation has started for a given number of images. + Started(usize), + + // Thumbnail has been generated for a photo. + Cleaned, + + // Thumbnail generation has completed + Completed, + +} + +pub struct Cleanup { + // Danger! Don't hold the repo mutex for too long as it blocks viewing images. + repo: Arc>, +} + +impl Cleanup { + + fn cleanup(&self, sender: &ComponentSender) -> Result<()> { + + let start = std::time::Instant::now(); + + // Scrub pics from database if they no longer exist on the file system. + let pics: Vec = self.repo + .lock() + .unwrap() + .all()?; + + let pics_count = pics.len(); + + if let Err(e) = sender.output(CleanupOutput::Started(pics_count)){ + println!("Failed sending cleanup started: {:?}", e); + } + + pics.par_iter() + .for_each(|pic| { + if !pic.path.exists() { + let result = self.repo.lock().unwrap().remove(pic.picture_id); + if let Err(e) = result { + println!("Failed remove {}: {:?}", pic.picture_id, e); + } else { + println!("Removed {}", pic.picture_id); + } + } + + if let Err(e) = sender.output(CleanupOutput::Cleaned) { + println!("Failed sending CleanupOutput::Cleaned: {:?}", e); + } + }); + + println!("Cleaned {} photos in {} seconds.", pics_count, start.elapsed().as_secs()); + + if let Err(e) = sender.output(CleanupOutput::Completed) { + println!("Failed sending CleanupOutput::Completed: {:?}", e); + } + + Ok(()) + } +} + +impl Worker for Cleanup { + type Init = Arc>; + type Input = CleanupInput; + type Output = CleanupOutput; + + fn init(repo: Self::Init, _sender: ComponentSender) -> Self { + Self { repo } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + match msg { + CleanupInput::Start => { + println!("Cleanup..."); + + if let Err(e) = self.cleanup(&sender) { + println!("Failed to update previews: {}", e); + } + } + }; + } +} diff --git a/src/app/background/mod.rs b/src/app/background/mod.rs index 01e8b606..ed9a6280 100644 --- a/src/app/background/mod.rs +++ b/src/app/background/mod.rs @@ -4,3 +4,4 @@ pub mod scan_photos; pub mod generate_previews; +pub mod cleanup;