Merge branch 'remove-old-msgs-beyond-storage-pref' into 'mainline'

Remove old messages beyond storage preference

See merge request GNOME/geary!388
This commit is contained in:
Michael Gratton 2020-06-30 07:07:12 +00:00
commit e4edf28e0c
19 changed files with 632 additions and 24 deletions

View file

@ -24,6 +24,7 @@ sql_files = [
'version-023.sql',
'version-024.sql',
'version-025.sql',
'version-026.sql',
]
install_data(sql_files,

4
sql/version-026.sql Normal file
View file

@ -0,0 +1,4 @@
--
-- Track when account storage was last cleaned.
--
ALTER TABLE GarbageCollectionTable ADD COLUMN last_cleanup_time_t INTEGER DEFAULT NULL;

View file

@ -18,6 +18,7 @@ internal class Application.Controller : Geary.BaseObject {
private const uint MAX_AUTH_ATTEMPTS = 3;
private const uint CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES = 5;
/** Determines if conversations can be trashed from the given folder. */
public static bool does_folder_support_trash(Geary.Folder? target) {
@ -91,6 +92,11 @@ internal class Application.Controller : Geary.BaseObject {
// Requested mailto composers not yet fullfulled
private Gee.List<string?> pending_mailtos = new Gee.ArrayList<string>();
// Timeout to do work in idle after all windows have been sent to the background
private Geary.TimeoutManager all_windows_backgrounded_timeout;
private GLib.Cancellable? storage_cleanup_cancellable;
/**
* Emitted when an account is added or is enabled.
@ -148,6 +154,9 @@ internal class Application.Controller : Geary.BaseObject {
ConversationWebView.load_resources();
Accounts.SignatureWebView.load_resources();
this.all_windows_backgrounded_timeout =
new Geary.TimeoutManager.seconds(CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES * 60, on_unfocused_idle);
this.folks = Folks.IndividualAggregator.dup();
if (!this.folks.is_prepared) {
// Do this in the background since it can take a long time
@ -1413,6 +1422,35 @@ internal class Application.Controller : Geary.BaseObject {
}
}
/**
* Track a window receiving focus, for idle background work.
*/
public void window_focus_in() {
this.all_windows_backgrounded_timeout.reset();
if (this.storage_cleanup_cancellable != null) {
this.storage_cleanup_cancellable.cancel();
// Cleanup was still running and we don't know where we got to so
// we'll clear each of these so it runs next time we're in the
// background
foreach (AccountContext context in this.accounts.values) {
context.cancellable.cancelled.disconnect(this.storage_cleanup_cancellable.cancel);
Geary.Account account = context.account;
account.last_storage_cleanup = null;
}
this.storage_cleanup_cancellable = null;
}
}
/**
* Track a window going unfocused, for idle background work.
*/
public void window_focus_out() {
this.all_windows_backgrounded_timeout.start();
}
/** Displays a composer on the last active main window. */
internal void show_composer(Composer.Widget composer) {
var target = this.application.get_active_main_window();
@ -1672,6 +1710,30 @@ internal class Application.Controller : Geary.BaseObject {
}
}
private void on_unfocused_idle() {
// Schedule later, catching cases where work should occur later while still in background
this.all_windows_backgrounded_timeout.reset();
window_focus_out();
if (this.storage_cleanup_cancellable == null)
do_background_storage_cleanup.begin();
}
private async void do_background_storage_cleanup() {
debug("Checking for backgrounded idle work");
this.storage_cleanup_cancellable = new GLib.Cancellable();
foreach (AccountContext context in this.accounts.values) {
Geary.Account account = context.account;
context.cancellable.cancelled.connect(this.storage_cleanup_cancellable.cancel);
yield account.cleanup_storage(this.storage_cleanup_cancellable);
if (this.storage_cleanup_cancellable.is_cancelled())
break;
context.cancellable.cancelled.disconnect(this.storage_cleanup_cancellable.cancel);
}
this.storage_cleanup_cancellable = null;
}
}

View file

@ -500,6 +500,15 @@ public class Application.MainWindow :
// Window actions
add_action_entries(MainWindow.WINDOW_ACTIONS, this);
this.focus_in_event.connect((w, e) => {
application.controller.window_focus_in();
return false;
});
this.focus_out_event.connect((w, e) => {
application.controller.window_focus_out();
return false;
});
setup_layout(application.config);
on_change_orientation();

View file

@ -47,6 +47,15 @@ public class Composer.Window : Gtk.ApplicationWindow, Container {
set_titlebar(this.composer.header);
}
this.focus_in_event.connect((w, e) => {
application.controller.window_focus_in();
return false;
});
this.focus_out_event.connect((w, e) => {
application.controller.window_focus_out();
return false;
});
show();
set_position(Gtk.WindowPosition.CENTER);
}

View file

@ -32,6 +32,11 @@ public class UpgradeDialog : Object {
}
private void on_start() {
// Disable main windows
foreach (Application.MainWindow window in this.application.get_main_windows()) {
window.sensitive = false;
}
Gtk.Builder builder = GioUtil.create_builder("upgrade_dialog.glade");
this.dialog = (Gtk.Dialog) builder.get_object("dialog");
this.dialog.set_transient_for(
@ -58,6 +63,11 @@ public class UpgradeDialog : Object {
this.dialog.hide();
this.dialog = null;
}
// Enable main windows
foreach (Application.MainWindow window in this.application.get_main_windows()) {
window.sensitive = true;
}
}
/**

View file

@ -145,6 +145,17 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
public ProgressMonitor db_upgrade_monitor { get; protected set; }
public ProgressMonitor db_vacuum_monitor { get; protected set; }
/**
* The last time the account storage was cleaned.
*
* This does not imply that a full reap plus vacuum garbage
* collection (GC) is performed, merely that:
* 1. Any old messages are removed
* 2. If any old messages were removed, or the defined period
* (in ImapDB.GC) has past, a GC reap is performed
* 3. GC vacuum is run if recommended
*/
public GLib.DateTime? last_storage_cleanup { get; set; }
public signal void opened();
@ -236,6 +247,11 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
*/
public signal void email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when emails are removed from a local folder in this account.
*/
public signal void email_locally_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when one or more emails have been locally saved to a folder with
* the full set of Fields.
@ -511,6 +527,16 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
return new Logging.State(this, this.information.id);
}
/**
* Perform cleanup of account storage.
*
* Work is performed if the appropriate interval has past since last
* execution. Alternatively if the interval has not past but vacuum GC
* has been flagged to run this will be executed. Designed to be run
* while the app is in the background and idle.
*/
public abstract async void cleanup_storage(GLib.Cancellable? cancellable);
/** Fires a {@link opened} signal. */
protected virtual void notify_opened() {
opened();
@ -558,6 +584,11 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
email_removed(folder, ids);
}
/** Fires a {@link email_locally_removed} signal. */
protected virtual void notify_email_locally_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_locally_removed(folder, ids);
}
/** Fires a {@link email_locally_complete} signal. */
protected virtual void notify_email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {

View file

@ -425,6 +425,11 @@ public abstract class Geary.Folder : BaseObject, Logging.Source {
*/
public signal void email_removed(Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when emails are removed from the local folder.
*/
public signal void email_locally_removed(Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when the total count of email in a folder has changed in any way.
*

View file

@ -321,6 +321,7 @@ public class Geary.App.ConversationMonitor : BaseObject, Logging.Source {
this.base_folder.email_inserted.connect(on_folder_email_inserted);
this.base_folder.email_locally_complete.connect(on_folder_email_complete);
this.base_folder.email_removed.connect(on_folder_email_removed);
this.base_folder.email_locally_removed.connect(on_folder_email_removed);
this.base_folder.opened.connect(on_folder_opened);
this.base_folder.account.email_appended.connect(on_account_email_appended);
this.base_folder.account.email_inserted.connect(on_account_email_inserted);
@ -669,6 +670,7 @@ public class Geary.App.ConversationMonitor : BaseObject, Logging.Source {
this.base_folder.email_inserted.disconnect(on_folder_email_inserted);
this.base_folder.email_locally_complete.disconnect(on_folder_email_complete);
this.base_folder.email_removed.disconnect(on_folder_email_removed);
this.base_folder.email_locally_removed.disconnect(on_folder_email_removed);
this.base_folder.opened.disconnect(on_folder_opened);
this.base_folder.account.email_appended.disconnect(on_account_email_appended);
this.base_folder.account.email_inserted.disconnect(on_account_email_inserted);

View file

@ -129,6 +129,7 @@ public class Geary.App.SearchFolder :
account.folders_use_changed.connect(on_folders_use_changed);
account.email_locally_complete.connect(on_email_locally_complete);
account.email_removed.connect(on_account_email_removed);
account.email_locally_removed.connect(on_account_email_removed);
new_contents();
@ -142,6 +143,7 @@ public class Geary.App.SearchFolder :
account.folders_use_changed.disconnect(on_folders_use_changed);
account.email_locally_complete.disconnect(on_email_locally_complete);
account.email_removed.disconnect(on_account_email_removed);
account.email_locally_removed.disconnect(on_account_email_removed);
}
/**

View file

@ -382,6 +382,57 @@ private class Geary.ImapDB.Account : BaseObject {
return create_local_folder(path, folder_id, properties);
}
/**
* Fetch the last time the account cleanup was run.
*/
public async GLib.DateTime? fetch_last_cleanup_async(Cancellable? cancellable)
throws Error {
check_open();
int64 last_cleanup_time_t = -1;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Result result = cx.query("""
SELECT last_cleanup_time_t
FROM GarbageCollectionTable
WHERE id = 0
""");
if (result.finished)
return Db.TransactionOutcome.FAILURE;
last_cleanup_time_t = !result.is_null_at(0) ? result.int64_at(0) : -1;
return Db.TransactionOutcome.SUCCESS;
}, cancellable);
return (last_cleanup_time_t >= 0) ? new DateTime.from_unix_local(last_cleanup_time_t) : null;
}
/**
* Set the last time the account cleanup was run.
*/
public async void set_last_cleanup_async(GLib.DateTime? dt, Cancellable? cancellable)
throws Error {
check_open();
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
Db.Statement stmt = cx.prepare("""
UPDATE GarbageCollectionTable
SET last_cleanup_time_t = ?
WHERE id = 0
""");
if (dt != null) {
stmt.bind_int64(0, dt.to_unix());
} else {
stmt.bind_null(0);
}
stmt.exec(cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
}
private Geary.ImapDB.Folder? get_local_folder(Geary.FolderPath path) {
FolderReference? folder_ref = folder_refs.get(path);
if (folder_ref == null)

View file

@ -18,6 +18,28 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
/** SQLite UTF-8 collation name. */
public const string UTF8_COLLATE = "UTF8COLL";
/** Options to use when running garbage collection. */
[Flags]
public enum GarbageCollectionOptions {
/** Reaping will not be forced and vacuuming not permitted. */
NONE,
/**
* Reaping will be performed, regardless of recommendation.
*/
FORCE_REAP,
/**
* Whether to permit database vacuum.
*
* Vacuuming is performed in the foreground.
*/
ALLOW_VACUUM;
}
public bool want_background_vacuum { get; set; default = false; }
private static void utf8_transliterate_fold(Sqlite.Context context,
Sqlite.Value[] values) {
@ -73,6 +95,26 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
throws Error {
yield base.open(flags, cancellable);
Geary.ClientService services_to_pause[] = {};
yield run_gc(NONE, services_to_pause, cancellable);
}
/**
* Run garbage collection
*
* Reap should only be forced when there is known cleanup to perform and
* the interval based recommendation should be bypassed.
*/
public async void run_gc(GarbageCollectionOptions options,
Geary.ClientService[] services_to_pause,
GLib.Cancellable? cancellable)
throws Error {
if (this.gc != null) {
debug("GC abandoned, possibly already running");
return;
}
// Tie user-supplied Cancellable to internal Cancellable, which is used when close() is
// called
if (cancellable != null)
@ -89,26 +131,49 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
// VACUUM needs to execute in the foreground with the user given a busy prompt (and cannot
// be run at the same time as REAP)
if ((recommended & GC.RecommendedOperation.VACUUM) != 0) {
if (!vacuum_monitor.is_in_progress)
vacuum_monitor.notify_start();
if (GarbageCollectionOptions.ALLOW_VACUUM in options) {
this.want_background_vacuum = false;
foreach (ClientService service in services_to_pause) {
yield service.stop(gc_cancellable);
}
try {
yield this.gc.vacuum_async(gc_cancellable);
} catch (Error err) {
message(
"Vacuum of IMAP database %s failed: %s", this.path, err.message
);
throw err;
} finally {
if (vacuum_monitor.is_in_progress)
vacuum_monitor.notify_finish();
}
if (!vacuum_monitor.is_in_progress)
vacuum_monitor.notify_start();
try {
yield this.gc.vacuum_async(gc_cancellable);
} catch (Error err) {
message(
"Vacuum of IMAP database %s failed: %s", this.path, err.message
);
throw err;
} finally {
if (vacuum_monitor.is_in_progress)
vacuum_monitor.notify_finish();
}
foreach (ClientService service in services_to_pause) {
yield service.start(gc_cancellable);
}
} else {
// Flag a vacuum to run later when we've been idle in the background
debug("Flagging desire to GC vacuum");
this.want_background_vacuum = true;
}
}
// Abandon REAP if cancelled
if (cancellable != null && cancellable.is_cancelled()) {
cancellable.cancelled.disconnect(cancel_gc);
return;
}
// REAP can run in the background while the application is executing
if ((recommended & GC.RecommendedOperation.REAP) != 0) {
if (GarbageCollectionOptions.FORCE_REAP in options || (recommended & GC.RecommendedOperation.REAP) != 0) {
// run in the background and allow application to continue running
this.gc.reap_async.begin(gc_cancellable, on_reap_async_completed);
} else {
this.gc = null;
}
if (cancellable != null)
@ -123,6 +188,24 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
this.path, err.message);
}
// Check if after reap we now want to schedule a background vacuum. The idea
// here is eg. if we've just reduced prefetch period, reap has detached a
// whole lot of messages and we want to vacuum. This check catches that
// vacuum recommendation, flagging it to run when in background.
this.gc.should_run_async.begin(
gc_cancellable,
(obj, res) => {
try {
GC.RecommendedOperation recommended = this.gc.should_run_async.end(res);
if ((recommended & GC.RecommendedOperation.VACUUM) != 0)
this.want_background_vacuum = true;
} catch (Error err) {
debug("Failed to run GC check on %s after REAP: %s",
this.path, err.message);
}
}
);
this.gc = null;
}

View file

@ -41,6 +41,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
private const int LIST_EMAIL_FIELDS_CHUNK_COUNT = 500;
private const int REMOVE_COMPLETE_LOCATIONS_CHUNK_COUNT = 500;
private const int CREATE_MERGE_EMAIL_CHUNK_COUNT = 25;
private const int OLD_MSG_DETACH_BATCH_SIZE = 1000;
[Flags]
public enum ListFlags {
@ -882,6 +883,107 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
}, cancellable);
}
public async Gee.Collection<Geary.EmailIdentifier>? detach_emails_before_timestamp(DateTime cutoff,
GLib.Cancellable? cancellable) throws Error {
debug("Detaching emails before %s for folder ID %s", cutoff.to_string(), this.folder_id.to_string());
Gee.ArrayList<ImapDB.EmailIdentifier>? deleted_email_ids = null;
Gee.ArrayList<string> deleted_primary_keys = null;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
// MessageLocationTable.ordering isn't relied on due to IMAP folder
// UIDs not guaranteed to be in order.
StringBuilder sql = new StringBuilder();
sql.append("""
SELECT id, message_id, ordering
FROM MessageLocationTable
WHERE folder_id = ?
AND message_id IN (
SELECT id
FROM MessageTable
INDEXED BY MessageTableInternalDateTimeTIndex
WHERE internaldate_time_t < ?
)
""");
Db.Statement stmt = cx.prepare(sql.str);
stmt.bind_rowid(0, folder_id);
stmt.bind_int64(1, cutoff.to_unix());
Db.Result results = stmt.exec(cancellable);
while (!results.finished) {
if (deleted_email_ids == null) {
deleted_email_ids = new Gee.ArrayList<ImapDB.EmailIdentifier>();
deleted_primary_keys = new Gee.ArrayList<string>();
}
deleted_email_ids.add(
new ImapDB.EmailIdentifier(results.int64_at(1),
new Imap.UID(results.int64_at(2)))
);
deleted_primary_keys.add(results.rowid_at(0).to_string());
results.next(cancellable);
}
return Db.TransactionOutcome.DONE;
}, cancellable);
if (deleted_email_ids != null) {
// Delete in batches to avoid hiting SQLite maximum query
// length (although quite unlikely)
int delete_index = 0;
while (delete_index < deleted_primary_keys.size) {
int batch_counter = 0;
StringBuilder message_location_ids_sql_sublist = new StringBuilder();
StringBuilder message_ids_sql_sublist = new StringBuilder();
while (delete_index < deleted_primary_keys.size
&& batch_counter < OLD_MSG_DETACH_BATCH_SIZE) {
if (batch_counter > 0) {
message_location_ids_sql_sublist.append(",");
message_ids_sql_sublist.append(",");
}
message_location_ids_sql_sublist.append(
deleted_primary_keys.get(delete_index)
);
message_ids_sql_sublist.append(
deleted_email_ids.get(delete_index).message_id.to_string()
);
delete_index++;
batch_counter++;
}
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
StringBuilder sql = new StringBuilder();
sql.append("""
DELETE FROM MessageLocationTable
WHERE id IN (
""");
sql.append(message_location_ids_sql_sublist.str);
sql.append(")");
Db.Statement stmt = cx.prepare(sql.str);
stmt.exec(cancellable);
sql = new StringBuilder();
sql.append("""
DELETE FROM MessageSearchTable
WHERE docid IN (
""");
sql.append(message_ids_sql_sublist.str);
sql.append(")");
stmt = cx.prepare(sql.str);
stmt.exec(cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
}
}
return deleted_email_ids;
}
public async void mark_email_async(Gee.Collection<ImapDB.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, Cancellable? cancellable)
throws Error {

View file

@ -25,6 +25,7 @@ private class Geary.ImapEngine.AccountSynchronizer :
);
this.account.information.notify["prefetch-period-days"].connect(on_account_prefetch_changed);
this.account.old_messages_background_cleanup_request.connect(old_messages_background_cleanup);
this.account.folders_available_unavailable.connect(on_folders_updated);
this.account.folders_contents_altered.connect(on_folders_contents_altered);
}
@ -44,7 +45,11 @@ private class Geary.ImapEngine.AccountSynchronizer :
);
}
private void send_all(Gee.Collection<Folder> folders, bool became_available) {
private void send_all(Gee.Collection<Folder> folders,
bool became_available,
bool for_storage_clean=false,
IdleGarbageCollection? post_idle_detach_op=null) {
foreach (Folder folder in folders) {
// Only sync folders that:
// 1. Can actually be opened (i.e. are selectable)
@ -60,11 +65,19 @@ private class Geary.ImapEngine.AccountSynchronizer :
!folder.properties.is_local_only &&
!folder.properties.is_virtual) {
AccountOperation op = became_available
? new CheckFolderSync(
this.account, imap_folder, this.max_epoch
)
: new RefreshFolderSync(this.account, imap_folder);
AccountOperation op;
if (became_available || for_storage_clean) {
CheckFolderSync check_op = new CheckFolderSync(
this.account,
imap_folder,
this.max_epoch,
for_storage_clean,
post_idle_detach_op
);
op = check_op;
} else {
op = new RefreshFolderSync(this.account, imap_folder);
}
try {
this.account.queue_operation(op);
@ -84,6 +97,22 @@ private class Geary.ImapEngine.AccountSynchronizer :
}
}
private void old_messages_background_cleanup(GLib.Cancellable? cancellable) {
if (this.account.is_open()) {
IdleGarbageCollection op = new IdleGarbageCollection(account);
send_all(this.account.list_folders(), false, true, op);
// Add GC operation after message removal during background cleanup
try {
this.account.queue_operation(op);
} catch (Error err) {
warning("Failed to queue sync operation: %s", err.message);
}
}
}
private void on_account_prefetch_changed() {
this.prefetch_timer.start();
}
@ -214,15 +243,20 @@ private class Geary.ImapEngine.RefreshFolderSync : FolderOperation {
*/
private class Geary.ImapEngine.CheckFolderSync : RefreshFolderSync {
private DateTime sync_max_epoch;
private bool for_storage_clean;
private IdleGarbageCollection? post_idle_detach_op;
internal CheckFolderSync(GenericAccount account,
MinimalFolder folder,
DateTime sync_max_epoch) {
DateTime sync_max_epoch,
bool for_storage_clean,
IdleGarbageCollection? post_idle_detach_op) {
base(account, folder);
this.sync_max_epoch = sync_max_epoch;
this.for_storage_clean = for_storage_clean;
this.post_idle_detach_op = post_idle_detach_op;
}
protected override async void sync_folder(Cancellable cancellable)
@ -238,9 +272,34 @@ private class Geary.ImapEngine.CheckFolderSync : RefreshFolderSync {
prefetch_max_epoch = this.sync_max_epoch;
}
ImapDB.Folder local_folder = ((MinimalFolder) this.folder).local_folder;
// Detach older emails outside the prefetch window
if (this.account.information.prefetch_period_days >= 0) {
Gee.Collection<Geary.EmailIdentifier>? detached_ids =
yield local_folder.detach_emails_before_timestamp(prefetch_max_epoch,
cancellable);
if (detached_ids != null) {
this.folder.email_locally_removed(detached_ids);
if (post_idle_detach_op != null) {
post_idle_detach_op.messages_detached();
}
if (!for_storage_clean) {
GenericAccount imap_account = (GenericAccount) account;
ForegroundGarbageCollection op =
new ForegroundGarbageCollection(imap_account);
try {
imap_account.queue_operation(op);
} catch (Error err) {
warning("Failed to queue sync operation: %s", err.message);
}
}
}
}
// get oldest local email and its time, as well as number
// of messages in local store
ImapDB.Folder local_folder = ((MinimalFolder) this.folder).local_folder;
Gee.List<Geary.Email>? list = yield local_folder.list_email_by_id_async(
null,
1,
@ -366,3 +425,70 @@ private class Geary.ImapEngine.CheckFolderSync : RefreshFolderSync {
}
}
/**
* Kicks off garbage collection after old messages have been removed.
*
* Queues a basic GC run which will run if old messages were detached
* after a folder became available. Not used for backgrounded account
* storage operations, which are handled instead by the
* {@link IdleGarbageCollection}.
*/
private class Geary.ImapEngine.ForegroundGarbageCollection: AccountOperation {
internal ForegroundGarbageCollection(GenericAccount account) {
base(account);
}
public override async void execute(GLib.Cancellable cancellable)
throws Error {
if (cancellable.is_cancelled())
return;
// Run basic GC
GenericAccount generic_account = (GenericAccount) account;
Geary.ClientService services_to_pause[] = {};
yield generic_account.local.db.run_gc(NONE, services_to_pause, cancellable);
}
public override bool equal_to(AccountOperation op) {
return (op != null
&& (this == op || this.get_type() == op.get_type())
&& this.account == op.account);
}
}
/**
* Performs garbage collection after old messages have been removed during
* backgrounded idle cleanup.
*
* Queues a GC run after a cleanup of messages has occurred while the
* app is idle in the background. Vacuuming will be permitted and if
* messages have been removed a reap will be forced.
*/
private class Geary.ImapEngine.IdleGarbageCollection: AccountOperation {
// Vacuum is allowed as we're running in the background
private Geary.ImapDB.Database.GarbageCollectionOptions options = ALLOW_VACUUM;
internal IdleGarbageCollection(GenericAccount account) {
base(account);
}
public override async void execute(GLib.Cancellable cancellable)
throws Error {
if (cancellable.is_cancelled())
return;
GenericAccount generic_account = (GenericAccount) this.account;
generic_account.local.db.run_gc.begin(this.options,
{generic_account.imap, generic_account.smtp},
cancellable);
}
public void messages_detached() {
// Reap is forced if messages were detached
this.options |= FORCE_REAP;
}
}

View file

@ -17,6 +17,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
// we don't need to double check.
private const int REFRESH_FOLDER_LIST_SEC = 15 * 60;
/** Minimum interval between account storage cleanup work */
private const uint APP_BACKGROUNDED_CLEANUP_WORK_INTERVAL_MINUTES = 60 * 24;
private const Folder.SpecialUse[] SUPPORTED_SPECIAL_FOLDERS = {
DRAFTS,
SENT,
@ -39,6 +42,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/** Local database for the account. */
public ImapDB.Account local { get; private set; }
public signal void old_messages_background_cleanup_request(GLib.Cancellable? cancellable);
private bool open = false;
private Cancellable? open_cancellable = null;
private Nonblocking.Semaphore? remote_ready_lock = null;
@ -135,6 +140,9 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
throw err;
}
this.last_storage_cleanup = yield this.local.fetch_last_cleanup_async(cancellable);
this.notify["last_storage_cleanup"].connect(on_last_storage_cleanup_notify);
this.open = true;
notify_opened();
@ -559,6 +567,24 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return (map.size == 0) ? null : map;
}
/** {@inheritDoc} */
public override async void cleanup_storage(GLib.Cancellable? cancellable) {
debug("Backgrounded storage cleanup check for %s account", this.information.display_name);
DateTime now = new DateTime.now_local();
DateTime? last_cleanup = this.last_storage_cleanup;
if (last_cleanup == null ||
(now.difference(last_cleanup) / TimeSpan.MINUTE > APP_BACKGROUNDED_CLEANUP_WORK_INTERVAL_MINUTES)) {
// Interval check is OK, start by detaching old messages
this.last_storage_cleanup = now;
this.old_messages_background_cleanup_request(cancellable);
} else if (local.db.want_background_vacuum) {
// Vacuum has been flagged as needed, run it
local.db.run_gc.begin(ALLOW_VACUUM, {this.imap, this.smtp}, cancellable);
}
}
/**
* Constructs a set of folders and adds them to the account.
*
@ -774,6 +800,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
folder.email_appended.connect(notify_email_appended);
folder.email_inserted.connect(notify_email_inserted);
folder.email_removed.connect(notify_email_removed);
folder.email_locally_removed.connect(notify_email_locally_removed);
folder.email_locally_complete.connect(notify_email_locally_complete);
folder.email_flags_changed.connect(notify_email_flags_changed);
}
@ -783,6 +810,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
folder.email_appended.disconnect(notify_email_appended);
folder.email_inserted.disconnect(notify_email_inserted);
folder.email_removed.disconnect(notify_email_removed);
folder.email_locally_removed.disconnect(notify_email_locally_removed);
folder.email_locally_complete.disconnect(notify_email_locally_complete);
folder.email_flags_changed.disconnect(notify_email_flags_changed);
}
@ -807,6 +835,11 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
schedule_unseen_update(folder);
}
/** {@inheritDoc} */
protected override void notify_email_locally_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
base.notify_email_locally_removed(folder, ids);
}
/** {@inheritDoc} */
protected override void notify_email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map) {
@ -969,6 +1002,13 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
}
}
private void on_last_storage_cleanup_notify() {
this.local.set_last_cleanup_async.begin(
this.last_storage_cleanup,
this.open_cancellable
);
}
}

View file

@ -1298,6 +1298,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
// open while processing first the flag updates then the
// expunge from the remote
yield this.replay_queue.checkpoint(cancellable);
Geary.ClientService services_to_pause[] = {};
yield this._account.local.db.run_gc(NONE, services_to_pause, cancellable);
}
private void check_open(string method) throws EngineError {

View file

@ -286,4 +286,7 @@ public class Geary.MockAccount : Account, MockObject {
);
}
public override async void cleanup_storage(GLib.Cancellable? cancellable) {
}
}

View file

@ -106,7 +106,7 @@ class Geary.ImapDB.DatabaseTest : TestCase {
);
db.open.end(async_result());
assert_int(25, db.get_schema_version(), "Post-upgrade version");
assert_int(26, db.get_schema_version(), "Post-upgrade version");
// Since schema v22 deletes the re-creates all attachments,
// attachment 12 should no longer exist on the file system and

View file

@ -26,6 +26,7 @@ class Geary.ImapDB.FolderTest : TestCase {
//add_test("merge_existing_preview", merge_existing_preview);
add_test("set_flags", set_flags);
add_test("set_flags_on_deleted", set_flags_on_deleted);
add_test("detach_emails_before_timestamp", detach_emails_before_timestamp);
}
public override void set_up() throws GLib.Error {
@ -323,6 +324,70 @@ class Geary.ImapDB.FolderTest : TestCase {
assert_flags(test, test_flags);
}
public void detach_emails_before_timestamp() throws GLib.Error {
// Ensures that messages outside the folder and within the epoch aren't
// removed, and that messages meeting the criteria are removed.
this.account.db.exec(
"INSERT INTO FolderTable (id, name) VALUES (2, 'other');"
);
GLib.DateTime threshold = new GLib.DateTime.local(2020, 1, 1, 0, 0, 0);
GLib.DateTime beyond_threshold = new GLib.DateTime.local(2019, 1, 1, 0, 0, 0);
GLib.DateTime within_threshold = new GLib.DateTime.local(2021, 1, 1, 0, 0, 0);
Email.Field fixture_fields = Email.Field.RECEIVERS;
string fixture_to = "test1@example.com";
this.account.db.exec(
"INSERT INTO MessageTable (id, fields, to_field, internaldate_time_t) " +
"VALUES (1, %d, '%s', %s);".printf(fixture_fields,
fixture_to,
within_threshold.to_unix().to_string())
);
this.account.db.exec(
"INSERT INTO MessageTable (id, fields, to_field, internaldate_time_t) " +
"VALUES (2, %d, '%s', %s);".printf(fixture_fields,
fixture_to,
within_threshold.to_unix().to_string())
);
this.account.db.exec(
"INSERT INTO MessageTable (id, fields, to_field, internaldate_time_t) " +
"VALUES (3, %d, '%s', %s);".printf(fixture_fields,
fixture_to,
beyond_threshold.to_unix().to_string())
);
this.account.db.exec("""
INSERT INTO MessageLocationTable
(id, message_id, folder_id, ordering, remove_marker)
VALUES
(1, 1, 1, 1, 1),
(2, 2, 2, 1, 1),
(3, 3, 1, 2, 1);
""");
this.folder.detach_emails_before_timestamp.begin(
threshold,
null,
this.async_completion
);
this.folder.detach_emails_before_timestamp.end(async_result());
int64[] expected = { 1, 2 };
Db.Result result = this.account.db.query(
"SELECT id FROM MessageLocationTable"
);
int i = 0;
while (!result.finished) {
assert_true(i < expected.length, "Too many rows");
assert_int64(expected[i], result.int64_at(0));
i++;
result.next();
}
assert_true(i == expected.length, "Not enough rows");
}
private Email new_mock_remote_email(int64 uid,
string? subject = null,
Geary.EmailFlags? flags = null) {