/*
* e-cache-reaper.c
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*
*/
#include "evolution-data-server-config.h"
#include
#include
#include
#include
#include "e-cache-reaper.h"
#include "e-cache-reaper-utils.h"
/* Where abandoned directories go to die. */
#define TRASH_DIRECTORY_NAME "trash"
/* XXX These intervals are rather arbitrary and prone to bikeshedding.
* It's just what I decided on. On startup we wait an hour to reap
* abandoned directories, and thereafter repeat every 24 hours. */
#define INITIAL_INTERVAL_SECONDS ( 1 * (60 * 60))
#define REGULAR_INTERVAL_SECONDS (24 * (60 * 60))
/* XXX Similarly, these expiry times are rather arbitrary and prone to
* bikeshedding. Most importantly, the expiry for data directories
* should be far more conservative (longer) than cache directories.
* Cache directories are disposable, data directories are not, so
* we want to let abandoned data directories linger longer. */
/* Minimum days for a data directory
* to live in trash before reaping it. */
#define DATA_EXPIRY_IN_DAYS 28
/* Minimum days for a cache directory
* to live in trash before reaping it. */
#define CACHE_EXPIRY_IN_DAYS 7
struct _ECacheReaper {
EExtension parent;
guint n_data_directories;
GFile **data_directories;
GFile **data_trash_directories;
guint n_cache_directories;
GFile **cache_directories;
GFile **cache_trash_directories;
guint reaping_timeout_id;
GSList *private_directories;
};
struct _ECacheReaperClass {
EExtensionClass parent_class;
};
G_DEFINE_DYNAMIC_TYPE_EXTENDED (ECacheReaper, e_cache_reaper, E_TYPE_EXTENSION, 0,
G_IMPLEMENT_INTERFACE_DYNAMIC (E_TYPE_EXTENSIBLE, NULL))
static ESourceRegistryServer *
cache_reaper_get_server (ECacheReaper *extension)
{
EExtensible *extensible;
extensible = e_extension_get_extensible (E_EXTENSION (extension));
return E_SOURCE_REGISTRY_SERVER (extensible);
}
static gboolean
cache_reaper_make_directory_and_parents (GFile *directory,
GCancellable *cancellable,
GError **error)
{
gboolean success;
GError *local_error = NULL;
/* XXX Maybe add some function like this to libedataserver.
* It's annoying to always have to check for and clear
* G_IO_ERROR_EXISTS when ensuring a directory exists. */
success = g_file_make_directory_with_parents (
directory, cancellable, &local_error);
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS))
g_clear_error (&local_error);
if (local_error != NULL) {
gchar *path;
g_propagate_error (error, local_error);
path = g_file_get_path (directory);
g_prefix_error (
error, "Failed to make directory '%s': ", path);
g_free (path);
}
return success;
}
static void
cache_reaper_trash_directory_reaped (GObject *source_object,
GAsyncResult *result,
gpointer unused)
{
GFile *trash_directory;
GError *error = NULL;
trash_directory = G_FILE (source_object);
e_reap_trash_directory_finish (trash_directory, result, &error);
/* Ignore cancellations. */
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
/* do nothing */
} else if (error != NULL) {
gchar *path;
path = g_file_get_path (trash_directory);
g_warning ("Failed to reap '%s': %s", path, error->message);
g_free (path);
}
g_clear_error (&error);
}
static gboolean
cache_reaper_reap_trash_directories (gpointer user_data)
{
ECacheReaper *extension = E_CACHE_REAPER (user_data);
guint ii;
g_debug ("Reaping abandoned data directories");
for (ii = 0; ii < extension->n_data_directories; ii++)
e_reap_trash_directory (
extension->data_trash_directories[ii],
DATA_EXPIRY_IN_DAYS,
G_PRIORITY_LOW, NULL,
cache_reaper_trash_directory_reaped,
NULL);
g_debug ("Reaping abandoned cache directories");
for (ii = 0; ii < extension->n_cache_directories; ii++)
e_reap_trash_directory (
extension->cache_trash_directories[ii],
CACHE_EXPIRY_IN_DAYS,
G_PRIORITY_LOW, NULL,
cache_reaper_trash_directory_reaped,
NULL);
/* Always explicitly reschedule since the initial
* interval is different than the regular interval. */
extension->reaping_timeout_id =
e_named_timeout_add_seconds (
REGULAR_INTERVAL_SECONDS,
cache_reaper_reap_trash_directories,
extension);
return FALSE;
}
static void
cache_reaper_move_directory (GFile *source_directory,
GFile *target_directory)
{
GFileType file_type;
GError *error = NULL;
/* Make sure the source directory is really a directory. */
file_type = g_file_query_file_type (
source_directory,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL);
if (file_type == G_FILE_TYPE_DIRECTORY) {
g_file_move (
source_directory,
target_directory,
G_FILE_COPY_NOFOLLOW_SYMLINKS,
NULL, NULL, NULL, &error);
/* Update the target directory's modification time.
* This step is not critical, do not set the GError. */
if (error == NULL) {
time_t now = time (NULL);
g_file_set_attribute (
target_directory,
G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_ATTRIBUTE_TYPE_UINT64,
&now, G_FILE_QUERY_INFO_NONE,
NULL, NULL);
}
}
if (error != NULL) {
gchar *path;
path = g_file_get_path (source_directory);
g_warning ("Failed to move '%s': %s", path, error->message);
g_free (path);
g_error_free (error);
}
}
static gboolean
cache_reaper_skip_directory (ECacheReaper *cache_reaper,
const gchar *name)
{
GSList *link;
/* Skip the trash directory, obviously. */
if (g_strcmp0 (name, TRASH_DIRECTORY_NAME) == 0)
return TRUE;
/* Also skip directories named "system". For backward
* compatibility, data directories for built-in sources
* are named "system" instead of "system-address-book"
* or "system-calendar" or what have you. */
if (g_strcmp0 (name, "system") == 0)
return TRUE;
for (link = cache_reaper->private_directories; link; link = g_slist_next (link)) {
if (g_strcmp0 (name, link->data) == 0) {
return TRUE;
}
}
return FALSE;
}
static void
cache_reaper_scan_directory (ECacheReaper *extension,
GFile *base_directory,
GFile *trash_directory)
{
GFileEnumerator *file_enumerator;
ESourceRegistryServer *server;
GFileInfo *file_info;
GError *error = NULL;
server = cache_reaper_get_server (extension);
file_enumerator = g_file_enumerate_children (
base_directory,
G_FILE_ATTRIBUTE_STANDARD_NAME,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
NULL, &error);
if (error != NULL) {
g_warn_if_fail (file_enumerator == NULL);
goto exit;
}
g_return_if_fail (G_IS_FILE_ENUMERATOR (file_enumerator));
file_info = g_file_enumerator_next_file (
file_enumerator, NULL, &error);
while (file_info != NULL) {
ESource *source;
const gchar *name;
name = g_file_info_get_name (file_info);
if (cache_reaper_skip_directory (extension, name))
goto next;
source = e_source_registry_server_ref_source (server, name);
if (source == NULL) {
GFile *source_directory;
GFile *target_directory;
source_directory = g_file_get_child (
base_directory, name);
target_directory = g_file_get_child (
trash_directory, name);
cache_reaper_move_directory (
source_directory, target_directory);
g_object_unref (source_directory);
g_object_unref (target_directory);
} else {
g_object_unref (source);
}
next:
g_object_unref (file_info);
file_info = g_file_enumerator_next_file (
file_enumerator, NULL, &error);
}
g_object_unref (file_enumerator);
exit:
if (error != NULL) {
gchar *path;
path = g_file_get_path (base_directory);
g_warning ("Failed to scan '%s': %s", path, error->message);
g_free (path);
g_error_free (error);
}
}
static void
cache_reaper_scan_data_directories (ECacheReaper *extension)
{
guint ii;
/* Scan the base data directories for unrecognized subdirectories.
* The subdirectories are named after data source UIDs, so compare
* their names to registered data sources and move any unrecognized
* subdirectories to the "trash" subdirectory to be reaped later. */
g_debug ("Scanning data directories");
for (ii = 0; ii < extension->n_data_directories; ii++)
cache_reaper_scan_directory (
extension,
extension->data_directories[ii],
extension->data_trash_directories[ii]);
}
static void
cache_reaper_scan_cache_directories (ECacheReaper *extension)
{
guint ii;
/* Scan the base cache directories for unrecognized subdirectories.
* The subdirectories are named after data source UIDs, so compare
* their names to registered data sources and move any unrecognized
* subdirectories to the "trash" subdirectory to be reaped later. */
g_debug ("Scanning cache directories");
for (ii = 0; ii < extension->n_cache_directories; ii++)
cache_reaper_scan_directory (
extension,
extension->cache_directories[ii],
extension->cache_trash_directories[ii]);
}
static void
cache_reaper_move_to_trash (ECacheReaper *extension,
ESource *source,
GFile *base_directory,
GFile *trash_directory)
{
GFile *source_directory;
GFile *target_directory;
const gchar *uid;
uid = e_source_get_uid (source);
source_directory = g_file_get_child (base_directory, uid);
target_directory = g_file_get_child (trash_directory, uid);
/* This is a no-op if the source directory does not exist. */
cache_reaper_move_directory (source_directory, target_directory);
g_object_unref (source_directory);
g_object_unref (target_directory);
}
static void
cache_reaper_recover_from_trash (ECacheReaper *extension,
const gchar *directory_uid,
GFile *base_directory,
GFile *trash_directory)
{
GFile *source_directory;
GFile *target_directory;
source_directory = g_file_get_child (trash_directory, directory_uid);
target_directory = g_file_get_child (base_directory, directory_uid);
/* This is a no-op if the source directory does not exist. */
cache_reaper_move_directory (source_directory, target_directory);
g_object_unref (source_directory);
g_object_unref (target_directory);
}
static void
cache_reaper_recover_for_uid (ECacheReaper *extension,
const gchar *uid)
{
guint ii;
/* The Cache Reaper is not too proud to dig through the
* trash on the off chance the newly-added source has a
* recoverable data or cache directory. */
for (ii = 0; ii < extension->n_data_directories; ii++)
cache_reaper_recover_from_trash (
extension, uid,
extension->data_directories[ii],
extension->data_trash_directories[ii]);
for (ii = 0; ii < extension->n_cache_directories; ii++)
cache_reaper_recover_from_trash (
extension, uid,
extension->cache_directories[ii],
extension->cache_trash_directories[ii]);
}
static void
cache_reaper_files_loaded_cb (ESourceRegistryServer *server,
ECacheReaper *extension)
{
GSList *link;
cache_reaper_scan_data_directories (extension);
cache_reaper_scan_cache_directories (extension);
/* Schedule the initial reaping. */
if (extension->reaping_timeout_id == 0) {
extension->reaping_timeout_id =
e_named_timeout_add_seconds (
INITIAL_INTERVAL_SECONDS,
cache_reaper_reap_trash_directories,
extension);
}
for (link = extension->private_directories; link; link = g_slist_next (link)) {
const gchar *directory = link->data;
if (directory && *directory)
cache_reaper_recover_for_uid (extension, directory);
}
}
static void
cache_reaper_source_added_cb (ESourceRegistryServer *server,
ESource *source,
ECacheReaper *extension)
{
cache_reaper_recover_for_uid (extension, e_source_get_uid (source));
}
static void
cache_reaper_source_removed_cb (ESourceRegistryServer *server,
ESource *source,
ECacheReaper *extension)
{
guint ii;
/* Stage the removed source's cache directory for reaping
* by moving it to the "trash" directory.
*
* Do NOT do this for data directories. Cache directories
* are disposable and can be regenerated from the canonical
* data source, but data directories ARE the canonical data
* source so we want to be more conservative with them. If
* the removed source has a data directory, we will move it
* to the "trash" directory on next registry startup, which
* may correspond with the next desktop session startup. */
for (ii = 0; ii < extension->n_cache_directories; ii++)
cache_reaper_move_to_trash (
extension, source,
extension->cache_directories[ii],
extension->cache_trash_directories[ii]);
}
static void
cache_reaper_finalize (GObject *object)
{
ECacheReaper *extension;
guint ii;
extension = E_CACHE_REAPER (object);
for (ii = 0; ii < extension->n_data_directories; ii++) {
g_object_unref (extension->data_directories[ii]);
g_object_unref (extension->data_trash_directories[ii]);
}
g_free (extension->data_directories);
g_free (extension->data_trash_directories);
for (ii = 0; ii < extension->n_cache_directories; ii++) {
g_object_unref (extension->cache_directories[ii]);
g_object_unref (extension->cache_trash_directories[ii]);
}
g_free (extension->cache_directories);
g_free (extension->cache_trash_directories);
if (extension->reaping_timeout_id > 0)
g_source_remove (extension->reaping_timeout_id);
g_slist_free_full (extension->private_directories, g_free);
extension->private_directories = NULL;
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS (e_cache_reaper_parent_class)->finalize (object);
}
static void
cache_reaper_constructed (GObject *object)
{
EExtension *extension;
EExtensible *extensible;
extension = E_EXTENSION (object);
extensible = e_extension_get_extensible (extension);
g_signal_connect (
extensible, "files-loaded",
G_CALLBACK (cache_reaper_files_loaded_cb), extension);
g_signal_connect (
extensible, "source-added",
G_CALLBACK (cache_reaper_source_added_cb), extension);
g_signal_connect (
extensible, "source-removed",
G_CALLBACK (cache_reaper_source_removed_cb), extension);
e_extensible_load_extensions (E_EXTENSIBLE (object));
/* Chain up to parent's constructed() method. */
G_OBJECT_CLASS (e_cache_reaper_parent_class)->constructed (object);
}
static void
e_cache_reaper_class_init (ECacheReaperClass *class)
{
GObjectClass *object_class;
EExtensionClass *extension_class;
object_class = G_OBJECT_CLASS (class);
object_class->finalize = cache_reaper_finalize;
object_class->constructed = cache_reaper_constructed;
extension_class = E_EXTENSION_CLASS (class);
extension_class->extensible_type = E_TYPE_SOURCE_REGISTRY_SERVER;
}
static void
e_cache_reaper_class_finalize (ECacheReaperClass *class)
{
}
static void
e_cache_reaper_init (ECacheReaper *extension)
{
GFile *base_directory;
const gchar *user_data_dir;
const gchar *user_cache_dir;
guint n_directories, ii;
/* These are component names from which
* the data directory arrays are built. */
const gchar *data_component_names[] = {
"addressbook",
"calendar",
"mail",
"memos",
"tasks"
};
/* These are component names from which
* the cache directory arrays are built. */
const gchar *cache_component_names[] = {
"addressbook",
"calendar",
"mail",
"memos",
"sources",
"tasks"
};
extension->private_directories = NULL;
/* Setup base directories for data. */
n_directories = G_N_ELEMENTS (data_component_names);
extension->n_data_directories = n_directories;
extension->data_directories = g_new0 (GFile *, n_directories);
extension->data_trash_directories = g_new0 (GFile *, n_directories);
user_data_dir = e_get_user_data_dir ();
base_directory = g_file_new_for_path (user_data_dir);
for (ii = 0; ii < n_directories; ii++) {
GFile *data_directory;
GFile *trash_directory;
GError *error = NULL;
data_directory = g_file_get_child (
base_directory, data_component_names[ii]);
trash_directory = g_file_get_child (
data_directory, TRASH_DIRECTORY_NAME);
/* Data directory is a parent of the trash
* directory so this is sufficient for both. */
cache_reaper_make_directory_and_parents (
trash_directory, NULL, &error);
if (error != NULL) {
g_warning ("%s: %s", G_STRFUNC, error->message);
g_error_free (error);
}
extension->data_directories[ii] = data_directory;
extension->data_trash_directories[ii] = trash_directory;
}
g_object_unref (base_directory);
/* Setup base directories for cache. */
n_directories = G_N_ELEMENTS (cache_component_names);
extension->n_cache_directories = n_directories;
extension->cache_directories = g_new0 (GFile *, n_directories);
extension->cache_trash_directories = g_new0 (GFile *, n_directories);
user_cache_dir = e_get_user_cache_dir ();
base_directory = g_file_new_for_path (user_cache_dir);
for (ii = 0; ii < n_directories; ii++) {
GFile *cache_directory;
GFile *trash_directory;
GError *error = NULL;
cache_directory = g_file_get_child (
base_directory, cache_component_names[ii]);
trash_directory = g_file_get_child (
cache_directory, TRASH_DIRECTORY_NAME);
/* Cache directory is a parent of the trash
* directory so this is sufficient for both. */
cache_reaper_make_directory_and_parents (
trash_directory, NULL, &error);
if (error != NULL) {
g_warning ("%s: %s", G_STRFUNC, error->message);
g_error_free (error);
}
extension->cache_directories[ii] = cache_directory;
extension->cache_trash_directories[ii] = trash_directory;
}
g_object_unref (base_directory);
}
/**
* e_cache_reaper_add_private_directory:
* @cache_reaper: an #ECacheReaper
* @name: directory name
*
* Let's the @cache_reaper know about a private directory named @name,
* thus it won't delete it from cache or data directories. The @name
* is just a directory name, not a path.
*
* Since 3.18
**/
void
e_cache_reaper_add_private_directory (ECacheReaper *cache_reaper,
const gchar *name)
{
g_return_if_fail (E_IS_CACHE_REAPER (cache_reaper));
g_return_if_fail (name != NULL);
if (g_slist_find_custom (cache_reaper->private_directories, name, (GCompareFunc) g_strcmp0))
return;
cache_reaper->private_directories = g_slist_prepend (cache_reaper->private_directories, g_strdup (name));
cache_reaper_recover_for_uid (cache_reaper, name);
}
/**
* e_cache_reaper_remove_private_directory:
* @cache_reaper: an #ECacheReaper
* @name: directory name
*
* Remove private directory named @name from the list of private
* directories in the @cache_reaper, previously added with
* e_cache_reaper_add_private_directory().
*
* Since 3.18
**/
void
e_cache_reaper_remove_private_directory (ECacheReaper *cache_reaper,
const gchar *name)
{
GSList *link;
gchar *saved_name;
g_return_if_fail (E_IS_CACHE_REAPER (cache_reaper));
g_return_if_fail (name != NULL);
link = g_slist_find_custom (cache_reaper->private_directories, name, (GCompareFunc) g_strcmp0);
if (!link)
return;
saved_name = link->data;
cache_reaper->private_directories = g_slist_remove (cache_reaper->private_directories, saved_name);
g_free (saved_name);
}
void
e_cache_reaper_type_register (GTypeModule *type_module)
{
e_cache_reaper_register_type (type_module);
}