diff --git a/CHANGELOG.md b/CHANGELOG.md index 605aa175a..53646b0fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Do not block async task executor while decrypting the messages. #4079 +- Housekeeping: delete the blobs backup dir #4123 ### API-Changes - jsonrpc: add more advanced API to send a message. #4097 diff --git a/src/imex.rs b/src/imex.rs index 23b876ba7..0bc9ab42d 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -34,7 +34,7 @@ use crate::{e2ee, tools}; // Name of the database file in the backup. const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite"; -const BLOBS_BACKUP_NAME: &str = "blobs_backup"; +pub(crate) const BLOBS_BACKUP_NAME: &str = "blobs_backup"; /// Import/export command. #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] diff --git a/src/sql.rs b/src/sql.rs index afc77ef1b..e37cb0856 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -15,6 +15,7 @@ use crate::constants::DC_CHAT_ID_TRASH; use crate::context::Context; use crate::debug_logging::set_debug_logging_xdc; use crate::ephemeral::start_ephemeral_timers; +use crate::imex::BLOBS_BACKUP_NAME; use crate::log::LogExt; use crate::message::{Message, MsgId, Viewtype}; use crate::param::{Param, Params}; @@ -792,74 +793,93 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> { .context("housekeeping: failed to SELECT value FROM config")?; info!(context, "{} files in use.", files_in_use.len(),); - /* go through directory and delete unused files */ - let p = context.get_blobdir(); - match tokio::fs::read_dir(p).await { - Ok(mut dir_handle) => { - /* avoid deletion of files that are just created to build a message object */ - let diff = std::time::Duration::from_secs(60 * 60); - let keep_files_newer_than = std::time::SystemTime::now() - .checked_sub(diff) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + /* go through directories and delete unused files */ + let blobdir = context.get_blobdir(); + for p in [&blobdir.join(BLOBS_BACKUP_NAME), blobdir] { + match tokio::fs::read_dir(p).await { + Ok(mut dir_handle) => { + /* avoid deletion of files that are just created to build a message object */ + let diff = std::time::Duration::from_secs(60 * 60); + let keep_files_newer_than = std::time::SystemTime::now() + .checked_sub(diff) + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); - while let Ok(Some(entry)) = dir_handle.next_entry().await { - let name_f = entry.file_name(); - let name_s = name_f.to_string_lossy(); + while let Ok(Some(entry)) = dir_handle.next_entry().await { + let name_f = entry.file_name(); + let name_s = name_f.to_string_lossy(); - if is_file_in_use(&files_in_use, None, &name_s) - || is_file_in_use(&files_in_use, Some(".increation"), &name_s) - || is_file_in_use(&files_in_use, Some(".waveform"), &name_s) - || is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s) - { - continue; - } - - unreferenced_count += 1; - - if let Ok(stats) = tokio::fs::metadata(entry.path()).await { - let recently_created = - stats.created().map_or(false, |t| t > keep_files_newer_than); - let recently_modified = stats - .modified() - .map_or(false, |t| t > keep_files_newer_than); - let recently_accessed = stats - .accessed() - .map_or(false, |t| t > keep_files_newer_than); - - if recently_created || recently_modified || recently_accessed { - info!( - context, - "Housekeeping: Keeping new unreferenced file #{}: {:?}", - unreferenced_count, - entry.file_name(), - ); + if p == blobdir + && (is_file_in_use(&files_in_use, None, &name_s) + || is_file_in_use(&files_in_use, Some(".increation"), &name_s) + || is_file_in_use(&files_in_use, Some(".waveform"), &name_s) + || is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s)) + { continue; } - } - info!( - context, - "Housekeeping: Deleting unreferenced file #{}: {:?}", - unreferenced_count, - entry.file_name() - ); - let path = entry.path(); - if let Err(err) = delete_file(context, &path).await { - error!( + + if let Ok(stats) = tokio::fs::metadata(entry.path()).await { + if stats.is_dir() { + if let Err(e) = tokio::fs::remove_dir(entry.path()).await { + // The dir could be created not by a user, but by a desktop + // environment f.e. So, no warning. + info!( + context, + "Housekeeping: Cannot rmdir {}: {:#}", + entry.path().display(), + e + ); + } + continue; + } + unreferenced_count += 1; + let recently_created = + stats.created().map_or(false, |t| t > keep_files_newer_than); + let recently_modified = stats + .modified() + .map_or(false, |t| t > keep_files_newer_than); + let recently_accessed = stats + .accessed() + .map_or(false, |t| t > keep_files_newer_than); + + if p == blobdir + && (recently_created || recently_modified || recently_accessed) + { + info!( + context, + "Housekeeping: Keeping new unreferenced file #{}: {:?}", + unreferenced_count, + entry.file_name(), + ); + continue; + } + } else { + unreferenced_count += 1; + } + info!( context, - "Failed to delete unused file {}: {:#}.", - path.display(), - err + "Housekeeping: Deleting unreferenced file #{}: {:?}", + unreferenced_count, + entry.file_name() ); + let path = entry.path(); + if let Err(err) = delete_file(context, &path).await { + error!( + context, + "Failed to delete unused file {}: {:#}.", + path.display(), + err + ); + } } } - } - Err(err) => { - warn!( - context, - "Housekeeping: Cannot open {}. ({})", - context.get_blobdir().display(), - err - ); + Err(err) => { + warn!( + context, + "Housekeeping: Cannot read dir {}: {:#}", + p.display(), + err + ); + } } } @@ -1061,6 +1081,18 @@ mod tests { assert_eq!(loaded_draft.unwrap().text.unwrap(), "This is my draft"); } + /// Tests that `housekeeping` deletes the blobs backup dir which is created normally by + /// `imex::import_backup`. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_housekeeping_delete_blobs_backup_dir() { + let t = TestContext::new_alice().await; + let dir = t.get_blobdir().join(BLOBS_BACKUP_NAME); + tokio::fs::create_dir(&dir).await.unwrap(); + tokio::fs::write(dir.join("f"), "").await.unwrap(); + housekeeping(&t).await.unwrap(); + tokio::fs::create_dir(&dir).await.unwrap(); + } + /// Regression test. /// /// Previously the code checking for existence of `config` table