summaryrefslogtreecommitdiffstats
path: root/plugins/housekeeping
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/housekeeping/gsd-disk-space-helper.c159
-rw-r--r--plugins/housekeeping/gsd-disk-space-helper.h38
-rw-r--r--plugins/housekeeping/gsd-disk-space-test.c48
-rw-r--r--plugins/housekeeping/gsd-disk-space.c1081
-rw-r--r--plugins/housekeeping/gsd-disk-space.h64
-rw-r--r--plugins/housekeeping/gsd-empty-trash-test.c44
-rw-r--r--plugins/housekeeping/gsd-housekeeping-manager.c519
-rw-r--r--plugins/housekeeping/gsd-housekeeping-manager.h38
-rw-r--r--plugins/housekeeping/gsd-purge-temp-test.c62
-rw-r--r--plugins/housekeeping/gsd-systemd-notify.c259
-rw-r--r--plugins/housekeeping/gsd-systemd-notify.h32
-rw-r--r--plugins/housekeeping/main.c7
-rw-r--r--plugins/housekeeping/meson.build42
13 files changed, 2393 insertions, 0 deletions
diff --git a/plugins/housekeeping/gsd-disk-space-helper.c b/plugins/housekeeping/gsd-disk-space-helper.c
new file mode 100644
index 0000000..56e054c
--- /dev/null
+++ b/plugins/housekeeping/gsd-disk-space-helper.c
@@ -0,0 +1,159 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ * Copyright (c) 2012, Red Hat, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ * Bastien Nocera <hadess@hadess.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "gsd-disk-space-helper.h"
+
+gboolean
+gsd_should_ignore_unix_mount (GUnixMountEntry *mount)
+{
+ const char *fs, *device;
+ g_autofree char *label = NULL;
+ guint i;
+
+ /* This is borrowed from GLib and used as a way to determine
+ * which mounts we should ignore by default. GLib doesn't
+ * expose this in a way that allows it to be used for this
+ * purpose
+ */
+
+ /* We also ignore network filesystems */
+#if !GLIB_CHECK_VERSION(2, 56, 0)
+ const gchar *ignore_fs[] = {
+ "adfs",
+ "afs",
+ "auto",
+ "autofs",
+ "autofs4",
+ "cgroup",
+ "configfs",
+ "cxfs",
+ "debugfs",
+ "devfs",
+ "devpts",
+ "devtmpfs",
+ "ecryptfs",
+ "fdescfs",
+ "fusectl",
+ "gfs",
+ "gfs2",
+ "gpfs",
+ "hugetlbfs",
+ "kernfs",
+ "linprocfs",
+ "linsysfs",
+ "lustre",
+ "lustre_lite",
+ "mfs",
+ "mqueue",
+ "ncpfs",
+ "nfsd",
+ "nullfs",
+ "ocfs2",
+ "overlay",
+ "proc",
+ "procfs",
+ "pstore",
+ "ptyfs",
+ "rootfs",
+ "rpc_pipefs",
+ "securityfs",
+ "selinuxfs",
+ "sysfs",
+ "tmpfs",
+ "usbfs",
+ "zfs",
+ NULL
+ };
+#endif /* GLIB < 2.56.0 */
+ const gchar *ignore_network_fs[] = {
+ "cifs",
+ "nfs",
+ "nfs4",
+ "smbfs",
+ NULL
+ };
+ const gchar *ignore_devices[] = {
+ "none",
+ "sunrpc",
+ "devpts",
+ "nfsd",
+ "/dev/loop",
+ "/dev/vn",
+ NULL
+ };
+ const gchar *ignore_labels[] = {
+ "RETRODE",
+ NULL
+ };
+
+ fs = g_unix_mount_get_fs_type (mount);
+#if GLIB_CHECK_VERSION(2, 56, 0)
+ if (g_unix_is_system_fs_type (fs))
+ return TRUE;
+#else
+ for (i = 0; ignore_fs[i] != NULL; i++)
+ if (g_str_equal (ignore_fs[i], fs))
+ return TRUE;
+#endif
+ for (i = 0; ignore_network_fs[i] != NULL; i++)
+ if (g_str_equal (ignore_network_fs[i], fs))
+ return TRUE;
+
+ device = g_unix_mount_get_device_path (mount);
+ for (i = 0; ignore_devices[i] != NULL; i++)
+ if (g_str_equal (ignore_devices[i], device))
+ return TRUE;
+
+ label = g_unix_mount_guess_name (mount);
+ for (i = 0; ignore_labels[i] != NULL; i++)
+ if (g_str_equal (ignore_labels[i], label))
+ return TRUE;
+
+ return FALSE;
+}
+
+/* Used in gnome-control-center's info panel */
+gboolean
+gsd_is_removable_mount (GUnixMountEntry *mount)
+{
+ const char *mount_path;
+ char *path;
+
+ mount_path = g_unix_mount_get_mount_path (mount);
+ if (mount_path == NULL)
+ return FALSE;
+
+ path = g_strdup_printf ("/run/media/%s", g_get_user_name ());
+ if (g_str_has_prefix (mount_path, path)) {
+ g_free (path);
+ return TRUE;
+ }
+ g_free (path);
+ return FALSE;
+}
diff --git a/plugins/housekeeping/gsd-disk-space-helper.h b/plugins/housekeeping/gsd-disk-space-helper.h
new file mode 100644
index 0000000..3898359
--- /dev/null
+++ b/plugins/housekeeping/gsd-disk-space-helper.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ * Copyright (c) 2012, Red Hat, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ * Bastien Nocera <hadess@hadess.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_DISK_SPACE_HELPER_H
+#define __GSD_DISK_SPACE_HELPER_H
+
+#include <glib.h>
+#include <gio/gunixmounts.h>
+
+G_BEGIN_DECLS
+
+gboolean gsd_should_ignore_unix_mount (GUnixMountEntry *mount);
+gboolean gsd_is_removable_mount (GUnixMountEntry *mount);
+
+G_END_DECLS
+
+#endif /* __GSD_DISK_SPACE_HELPER_H */
diff --git a/plugins/housekeeping/gsd-disk-space-test.c b/plugins/housekeeping/gsd-disk-space-test.c
new file mode 100644
index 0000000..c0410f8
--- /dev/null
+++ b/plugins/housekeeping/gsd-disk-space-test.c
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include <libnotify/notify.h>
+#include "gsd-disk-space.h"
+
+int
+main (int argc,
+ char **argv)
+{
+ GMainLoop *loop;
+
+ gtk_init (&argc, &argv);
+ notify_init ("gsd-disk-space-test");
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ gsd_ldsm_setup (TRUE);
+
+ g_main_loop_run (loop);
+
+ gsd_ldsm_clean ();
+ g_main_loop_unref (loop);
+
+ return 0;
+}
+
diff --git a/plugins/housekeeping/gsd-disk-space.c b/plugins/housekeeping/gsd-disk-space.c
new file mode 100644
index 0000000..c5ae8f6
--- /dev/null
+++ b/plugins/housekeeping/gsd-disk-space.c
@@ -0,0 +1,1081 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <sys/statvfs.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib-object.h>
+#include <gio/gunixmounts.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <libnotify/notify.h>
+
+#include "gsd-disk-space.h"
+#include "gsd-disk-space-helper.h"
+
+#define GIGABYTE 1024 * 1024 * 1024
+
+#define CHECK_EVERY_X_SECONDS 60
+
+#define DISK_SPACE_ANALYZER "baobab"
+
+#define SETTINGS_HOUSEKEEPING_DIR "org.gnome.settings-daemon.plugins.housekeeping"
+#define SETTINGS_FREE_PC_NOTIFY_KEY "free-percent-notify"
+#define SETTINGS_FREE_PC_NOTIFY_AGAIN_KEY "free-percent-notify-again"
+#define SETTINGS_FREE_SIZE_NO_NOTIFY "free-size-gb-no-notify"
+#define SETTINGS_MIN_NOTIFY_PERIOD "min-notify-period"
+#define SETTINGS_IGNORE_PATHS "ignore-paths"
+
+#define PRIVACY_SETTINGS "org.gnome.desktop.privacy"
+#define SETTINGS_PURGE_TRASH "remove-old-trash-files"
+#define SETTINGS_PURGE_TEMP_FILES "remove-old-temp-files"
+#define SETTINGS_PURGE_AFTER "old-files-age"
+
+typedef struct
+{
+ GUnixMountEntry *mount;
+ struct statvfs buf;
+ time_t notify_time;
+} LdsmMountInfo;
+
+static GHashTable *ldsm_notified_hash = NULL;
+static unsigned int ldsm_timeout_id = 0;
+static GUnixMountMonitor *ldsm_monitor = NULL;
+static double free_percent_notify = 0.05;
+static double free_percent_notify_again = 0.01;
+static unsigned int free_size_gb_no_notify = 2;
+static unsigned int min_notify_period = 10;
+static GSList *ignore_paths = NULL;
+static GSettings *settings = NULL;
+static GSettings *privacy_settings = NULL;
+static NotifyNotification *notification = NULL;
+
+static guint64 *time_read;
+
+static gboolean purge_trash;
+static gboolean purge_temp_files;
+static guint purge_after;
+static guint purge_trash_id = 0;
+static guint purge_temp_id = 0;
+
+static gchar*
+ldsm_get_fs_id_for_path (const gchar *path)
+{
+ GFile *file;
+ GFileInfo *fileinfo;
+ gchar *attr_id_fs;
+
+ file = g_file_new_for_path (path);
+ fileinfo = g_file_query_info (file, G_FILE_ATTRIBUTE_ID_FILESYSTEM, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
+ if (fileinfo) {
+ attr_id_fs = g_strdup (g_file_info_get_attribute_string (fileinfo, G_FILE_ATTRIBUTE_ID_FILESYSTEM));
+ g_object_unref (fileinfo);
+ } else {
+ attr_id_fs = NULL;
+ }
+
+ g_object_unref (file);
+
+ return attr_id_fs;
+}
+
+static gboolean
+ldsm_mount_has_trash (const char *path)
+{
+ const gchar *user_data_dir;
+ gchar *user_data_attr_id_fs;
+ gchar *path_attr_id_fs;
+ gboolean mount_uses_user_trash = FALSE;
+ gchar *trash_files_dir;
+ gboolean has_trash = FALSE;
+ GDir *dir;
+
+ user_data_dir = g_get_user_data_dir ();
+ user_data_attr_id_fs = ldsm_get_fs_id_for_path (user_data_dir);
+
+ path_attr_id_fs = ldsm_get_fs_id_for_path (path);
+
+ if (g_strcmp0 (user_data_attr_id_fs, path_attr_id_fs) == 0) {
+ /* The volume that is low on space is on the same volume as our home
+ * directory. This means the trash is at $XDG_DATA_HOME/Trash,
+ * not at the root of the volume which is full.
+ */
+ mount_uses_user_trash = TRUE;
+ }
+
+ g_free (user_data_attr_id_fs);
+ g_free (path_attr_id_fs);
+
+ /* I can't think of a better way to find out if a volume has any trash. Any suggestions? */
+ if (mount_uses_user_trash) {
+ trash_files_dir = g_build_filename (g_get_user_data_dir (), "Trash", "files", NULL);
+ } else {
+ gchar *uid;
+
+ uid = g_strdup_printf ("%d", getuid ());
+ trash_files_dir = g_build_filename (path, ".Trash", uid, "files", NULL);
+ if (!g_file_test (trash_files_dir, G_FILE_TEST_IS_DIR)) {
+ gchar *trash_dir;
+
+ g_free (trash_files_dir);
+ trash_dir = g_strdup_printf (".Trash-%s", uid);
+ trash_files_dir = g_build_filename (path, trash_dir, "files", NULL);
+ g_free (trash_dir);
+ if (!g_file_test (trash_files_dir, G_FILE_TEST_IS_DIR)) {
+ g_free (trash_files_dir);
+ g_free (uid);
+ return has_trash;
+ }
+ }
+ g_free (uid);
+ }
+
+ dir = g_dir_open (trash_files_dir, 0, NULL);
+ if (dir) {
+ if (g_dir_read_name (dir))
+ has_trash = TRUE;
+ g_dir_close (dir);
+ }
+
+ g_free (trash_files_dir);
+
+ return has_trash;
+}
+
+static void
+ldsm_analyze_path (const gchar *path)
+{
+ const gchar *argv[] = { DISK_SPACE_ANALYZER, path, NULL };
+
+ g_spawn_async (NULL, (gchar **) argv, NULL, G_SPAWN_SEARCH_PATH,
+ NULL, NULL, NULL, NULL);
+}
+
+static void
+ignore_callback (NotifyNotification *n,
+ const char *action)
+{
+ g_assert (action != NULL);
+ g_assert (strcmp (action, "ignore") == 0);
+
+ /* Do nothing */
+
+ notify_notification_close (n, NULL);
+}
+
+static void
+examine_callback (NotifyNotification *n,
+ const char *action,
+ const char *path)
+{
+ g_assert (action != NULL);
+ g_assert (strcmp (action, "examine") == 0);
+
+ ldsm_analyze_path (path);
+
+ notify_notification_close (n, NULL);
+}
+
+static gboolean
+should_purge_file (GFile *file,
+ GCancellable *cancellable,
+ GDateTime *old)
+{
+ GFileInfo *info;
+ GDateTime *date;
+ gboolean should_purge;
+
+ should_purge = FALSE;
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_TRASH_DELETION_DATE ","
+ G_FILE_ATTRIBUTE_UNIX_UID ","
+ G_FILE_ATTRIBUTE_TIME_CHANGED,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ cancellable,
+ NULL);
+ if (!info)
+ return FALSE;
+
+ date = g_file_info_get_deletion_date (info);
+ if (date == NULL) {
+ guint uid;
+ guint64 ctime;
+
+ uid = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_UID);
+ if (uid != getuid ()) {
+ should_purge = FALSE;
+ goto out;
+ }
+
+ ctime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED);
+ date = g_date_time_new_from_unix_local ((gint64) ctime);
+ }
+
+ should_purge = g_date_time_difference (old, date) >= 0;
+ g_date_time_unref (date);
+
+out:
+ g_object_unref (info);
+
+ return should_purge;
+}
+
+DeleteData *
+delete_data_new (GFile *file,
+ GCancellable *cancellable,
+ GDateTime *old,
+ gboolean dry_run,
+ gboolean trash,
+ gint depth,
+ const char *filesystem)
+{
+ DeleteData *data;
+
+ data = g_new (DeleteData, 1);
+ g_ref_count_init (&data->ref_count);
+ data->file = g_object_ref (file);
+ data->filesystem = g_strdup (filesystem);
+ data->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+ data->old = g_date_time_ref (old);
+ data->dry_run = dry_run;
+ data->trash = trash;
+ data->depth = depth;
+ data->name = g_file_get_parse_name (data->file);
+
+ return data;
+}
+
+char*
+get_filesystem (GFile *file)
+{
+ g_autoptr(GFileInfo) info = NULL;
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_ID_FILESYSTEM,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL);
+
+ return g_file_info_get_attribute_as_string (info,
+ G_FILE_ATTRIBUTE_ID_FILESYSTEM);
+}
+
+static DeleteData *
+delete_data_ref (DeleteData *data)
+{
+ g_ref_count_inc (&data->ref_count);
+
+ return data;
+}
+
+void
+delete_data_unref (DeleteData *data)
+{
+ if (g_ref_count_dec (&data->ref_count)) {
+ g_object_unref (data->file);
+ if (data->cancellable)
+ g_object_unref (data->cancellable);
+ g_date_time_unref (data->old);
+ g_free (data->name);
+ g_free (data->filesystem);
+ g_free (data);
+ }
+}
+
+static void
+delete_batch (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GFileEnumerator *enumerator = G_FILE_ENUMERATOR (source);
+ DeleteData *data = user_data;
+ const char *fs;
+ GList *files, *f;
+ GFile *child_file;
+ DeleteData *child;
+ GFileInfo *info;
+ GError *error = NULL;
+
+ files = g_file_enumerator_next_files_finish (enumerator, res, &error);
+
+ g_debug ("GsdHousekeeping: purging %d children of %s", g_list_length (files), data->name);
+
+ if (files) {
+ for (f = files; f; f = f->next) {
+ if (g_cancellable_is_cancelled (data->cancellable))
+ break;
+ info = f->data;
+
+ fs = g_file_info_get_attribute_string (info,
+ G_FILE_ATTRIBUTE_ID_FILESYSTEM);
+
+ /* Do not consider the file if it is on a different file system.
+ * Ignore data->filesystem if it is NULL (this only happens if
+ * it is the toplevel trash directory). */
+ if (data->filesystem && g_strcmp0 (fs, data->filesystem) != 0) {
+ g_debug ("GsdHousekeeping: skipping file \"%s\" as it is on a different file system",
+ g_file_info_get_name (info));
+ continue;
+ }
+
+ child_file = g_file_get_child (data->file, g_file_info_get_name (info));
+ child = delete_data_new (child_file,
+ data->cancellable,
+ data->old,
+ data->dry_run,
+ data->trash,
+ data->depth + 1,
+ fs);
+ delete_recursively_by_age (child);
+ delete_data_unref (child);
+ g_object_unref (child_file);
+ }
+ g_list_free_full (files, g_object_unref);
+ if (!g_cancellable_is_cancelled (data->cancellable)) {
+ g_file_enumerator_next_files_async (enumerator, 20,
+ 0,
+ data->cancellable,
+ delete_batch,
+ data);
+ return;
+ }
+ }
+
+ g_file_enumerator_close (enumerator, data->cancellable, NULL);
+ g_object_unref (enumerator);
+
+ if (data->depth > 0 && !g_cancellable_is_cancelled (data->cancellable)) {
+ if ((data->trash && data->depth > 1) ||
+ should_purge_file (data->file, data->cancellable, data->old)) {
+ g_debug ("GsdHousekeeping: purging %s\n", data->name);
+ if (!data->dry_run) {
+ g_file_delete (data->file, data->cancellable, NULL);
+ }
+ }
+ }
+ delete_data_unref (data);
+}
+
+static void
+delete_subdir (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GFile *file = G_FILE (source);
+ DeleteData *data = user_data;
+ GFileEnumerator *enumerator;
+ GError *error = NULL;
+
+ g_debug ("GsdHousekeeping: purging %s in %s\n",
+ data->trash ? "trash" : "temporary files", data->name);
+
+ enumerator = g_file_enumerate_children_finish (file, res, &error);
+ if (error) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
+ g_warning ("Failed to enumerate children of %s: %s\n", data->name, error->message);
+ }
+ if (enumerator) {
+ g_file_enumerator_next_files_async (enumerator, 20,
+ 0,
+ data->cancellable,
+ delete_batch,
+ delete_data_ref (data));
+ } else if (data->depth > 0 && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY)) {
+ if ((data->trash && data->depth > 1) ||
+ should_purge_file (data->file, data->cancellable, data->old)) {
+ g_debug ("Purging %s leaf node", data->name);
+ if (!data->dry_run) {
+ g_file_delete (data->file, data->cancellable, NULL);
+ }
+ }
+ }
+
+ if (error)
+ g_error_free (error);
+ delete_data_unref (data);
+}
+
+static void
+delete_subdir_check_symlink (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GFile *file = G_FILE (source);
+ DeleteData *data = user_data;
+ GFileInfo *info;
+
+ info = g_file_query_info_finish (file, res, NULL);
+ if (!info) {
+ delete_data_unref (data);
+ return;
+ }
+
+ if (g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK) {
+ if (should_purge_file (data->file, data->cancellable, data->old)) {
+ g_debug ("Purging %s leaf node", data->name);
+ if (!data->dry_run) {
+ g_file_delete (data->file, data->cancellable, NULL);
+ }
+ }
+ } else if (g_strcmp0 (g_file_info_get_name (info), ".X11-unix") == 0) {
+ g_debug ("Skipping X11 socket directory");
+ } else {
+ g_file_enumerate_children_async (data->file,
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE ","
+ G_FILE_ATTRIBUTE_ID_FILESYSTEM,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ 0,
+ data->cancellable,
+ delete_subdir,
+ delete_data_ref (data));
+ }
+ g_object_unref (info);
+ delete_data_unref (data);
+}
+
+void
+delete_recursively_by_age (DeleteData *data)
+{
+ if (data->trash && (data->depth == 1) &&
+ !should_purge_file (data->file, data->cancellable, data->old)) {
+ /* no need to recurse into trashed directories */
+ return;
+ }
+
+ g_file_query_info_async (data->file,
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ 0,
+ data->cancellable,
+ delete_subdir_check_symlink,
+ delete_data_ref (data));
+}
+
+void
+gsd_ldsm_purge_trash (GDateTime *old)
+{
+ GFile *file;
+ DeleteData *data;
+
+ file = g_file_new_for_uri ("trash:");
+ data = delete_data_new (file, NULL, old, FALSE, TRUE, 0, NULL);
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+ g_object_unref (file);
+}
+
+void
+gsd_ldsm_purge_temp_files (GDateTime *old)
+{
+ DeleteData *data;
+ GFile *file;
+ char *filesystem;
+
+ /* Never clean temporary files on a sane (i.e. systemd managed)
+ * system. In that case systemd already ships
+ * /usr/lib/tmpfiles.d/tmp.conf
+ * which does the trick in a much safer way.
+ * Ideally we can just drop this feature, I am not sure why it was
+ * added in the first place though, it does not really seem like a
+ * privacy feature (also, it was late in the release cycle).
+ * https://en.wikipedia.org/wiki/Wikipedia:Chesterton%27s_fence
+ *
+ * This does the same as sd_booted without needing libsystemd.
+ */
+ if (g_file_test ("/run/systemd/system/", G_FILE_TEST_IS_DIR))
+ return;
+
+ file = g_file_new_for_path (g_get_tmp_dir ());
+ filesystem = get_filesystem (file);
+ data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem);
+ g_free (filesystem);
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+ g_object_unref (file);
+
+ if (g_strcmp0 (g_get_tmp_dir (), "/var/tmp") != 0) {
+ file = g_file_new_for_path ("/var/tmp");
+ filesystem = get_filesystem (file);
+ data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem);
+ g_free (filesystem);
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+ g_object_unref (file);
+ }
+
+ if (g_strcmp0 (g_get_tmp_dir (), "/tmp") != 0) {
+ file = g_file_new_for_path ("/tmp");
+ filesystem = get_filesystem (file);
+ data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem);
+ g_free (filesystem);
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+ g_object_unref (file);
+ }
+}
+
+void
+gsd_ldsm_show_empty_trash (void)
+{
+ GFile *file;
+ GDateTime *old;
+ DeleteData *data;
+
+ old = g_date_time_new_now_local ();
+ file = g_file_new_for_uri ("trash:");
+ data = delete_data_new (file, NULL, old, TRUE, TRUE, 0, NULL);
+ g_object_unref (file);
+ g_date_time_unref (old);
+
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+}
+
+static gboolean
+ldsm_purge_trash_and_temp (gpointer data)
+{
+ GDateTime *now, *old;
+
+ now = g_date_time_new_now_local ();
+ old = g_date_time_add_days (now, - purge_after);
+
+ if (purge_trash) {
+ g_debug ("housekeeping: purge trash older than %u days", purge_after);
+ gsd_ldsm_purge_trash (old);
+ }
+ if (purge_temp_files) {
+ g_debug ("housekeeping: purge temp files older than %u days", purge_after);
+ gsd_ldsm_purge_temp_files (old);
+ }
+
+ g_date_time_unref (old);
+ g_date_time_unref (now);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+empty_trash_callback (NotifyNotification *n,
+ const char *action)
+{
+ GDateTime *old;
+
+ g_assert (action != NULL);
+ g_assert (strcmp (action, "empty-trash") == 0);
+
+ old = g_date_time_new_now_local ();
+ gsd_ldsm_purge_trash (old);
+ g_date_time_unref (old);
+
+ notify_notification_close (n, NULL);
+}
+
+static void
+on_notification_closed (NotifyNotification *n)
+{
+ g_object_unref (notification);
+ notification = NULL;
+}
+
+static void
+ldsm_notify (const char *summary,
+ const char *body,
+ const char *mount_path)
+{
+ gchar *program;
+ gboolean has_disk_analyzer;
+ gboolean has_trash;
+
+ /* Don't show a notice if one is already displayed */
+ if (notification != NULL)
+ return;
+
+ notification = notify_notification_new (summary, body, "drive-harddisk-symbolic");
+ g_signal_connect (notification,
+ "closed",
+ G_CALLBACK (on_notification_closed),
+ NULL);
+
+ notify_notification_set_app_name (notification, _("Disk Space"));
+ notify_notification_set_hint (notification, "transient", g_variant_new_boolean (TRUE));
+ notify_notification_set_urgency (notification, NOTIFY_URGENCY_CRITICAL);
+ notify_notification_set_timeout (notification, NOTIFY_EXPIRES_DEFAULT);
+ notify_notification_set_hint_string (notification, "desktop-entry", "org.gnome.baobab");
+
+ program = g_find_program_in_path (DISK_SPACE_ANALYZER);
+ has_disk_analyzer = (program != NULL);
+ g_free (program);
+
+ if (has_disk_analyzer) {
+ notify_notification_add_action (notification,
+ "examine",
+ _("Examine"),
+ (NotifyActionCallback) examine_callback,
+ g_strdup (mount_path),
+ g_free);
+ }
+
+ has_trash = ldsm_mount_has_trash (mount_path);
+
+ if (has_trash) {
+ notify_notification_add_action (notification,
+ "empty-trash",
+ _("Empty Trash"),
+ (NotifyActionCallback) empty_trash_callback,
+ NULL,
+ NULL);
+ }
+
+ notify_notification_add_action (notification,
+ "ignore",
+ _("Ignore"),
+ (NotifyActionCallback) ignore_callback,
+ NULL,
+ NULL);
+ notify_notification_set_category (notification, "device");
+
+ if (!notify_notification_show (notification, NULL)) {
+ g_warning ("failed to send disk space notification\n");
+ }
+}
+
+static void
+ldsm_notify_for_mount (LdsmMountInfo *mount,
+ gboolean multiple_volumes)
+{
+ gboolean has_trash;
+ gchar *name;
+ gint64 free_space;
+ const gchar *path;
+ char *free_space_str;
+ char *summary;
+ char *body;
+
+ name = g_unix_mount_guess_name (mount->mount);
+ path = g_unix_mount_get_mount_path (mount->mount);
+ has_trash = ldsm_mount_has_trash (path);
+
+ free_space = (gint64) mount->buf.f_frsize * (gint64) mount->buf.f_bavail;
+ free_space_str = g_format_size (free_space);
+
+ if (multiple_volumes) {
+ summary = g_strdup_printf (_("Low Disk Space on “%s”"), name);
+ if (has_trash) {
+ body = g_strdup_printf (_("The volume “%s” has only %s disk space remaining. You may free up some space by emptying the trash."),
+ name,
+ free_space_str);
+ } else {
+ body = g_strdup_printf (_("The volume “%s” has only %s disk space remaining."),
+ name,
+ free_space_str);
+ }
+ } else {
+ summary = g_strdup (_("Low Disk Space"));
+ if (has_trash) {
+ body = g_strdup_printf (_("This computer has only %s disk space remaining. You may free up some space by emptying the trash."),
+ free_space_str);
+ } else {
+ body = g_strdup_printf (_("This computer has only %s disk space remaining."),
+ free_space_str);
+ }
+ }
+
+ ldsm_notify (summary, body, path);
+
+ g_free (free_space_str);
+ g_free (summary);
+ g_free (body);
+ g_free (name);
+}
+
+static gboolean
+ldsm_mount_has_space (LdsmMountInfo *mount)
+{
+ gdouble free_space;
+
+ free_space = (double) mount->buf.f_bavail / (double) mount->buf.f_blocks;
+ /* enough free space, nothing to do */
+ if (free_space > free_percent_notify)
+ return TRUE;
+
+ if (((gint64) mount->buf.f_frsize * (gint64) mount->buf.f_bavail) > ((gint64) free_size_gb_no_notify * GIGABYTE))
+ return TRUE;
+
+ /* If we got here, then this volume is low on space */
+ return FALSE;
+}
+
+static gboolean
+ldsm_mount_is_virtual (LdsmMountInfo *mount)
+{
+ if (mount->buf.f_blocks == 0) {
+ /* Filesystems with zero blocks are virtual */
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gint
+ldsm_ignore_path_compare (gconstpointer a,
+ gconstpointer b)
+{
+ return g_strcmp0 ((const gchar *)a, (const gchar *)b);
+}
+
+static gboolean
+ldsm_mount_is_user_ignore (const gchar *path)
+{
+ if (g_slist_find_custom (ignore_paths, path, (GCompareFunc) ldsm_ignore_path_compare) != NULL)
+ return TRUE;
+ else
+ return FALSE;
+}
+
+
+static void
+ldsm_free_mount_info (gpointer data)
+{
+ LdsmMountInfo *mount = data;
+
+ g_return_if_fail (mount != NULL);
+
+ g_unix_mount_free (mount->mount);
+ g_free (mount);
+}
+
+static void
+ldsm_maybe_warn_mounts (GList *mounts,
+ gboolean multiple_volumes)
+{
+ GList *l;
+ gboolean done = FALSE;
+
+ for (l = mounts; l != NULL; l = l->next) {
+ LdsmMountInfo *mount_info = l->data;
+ LdsmMountInfo *previous_mount_info;
+ gdouble free_space;
+ gdouble previous_free_space;
+ time_t curr_time;
+ const gchar *path;
+ gboolean show_notify;
+
+ if (done) {
+ /* Don't show any more dialogs if the user took action with the last one. The user action
+ * might free up space on multiple volumes, making the next dialog redundant.
+ */
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ path = g_unix_mount_get_mount_path (mount_info->mount);
+
+ previous_mount_info = g_hash_table_lookup (ldsm_notified_hash, path);
+ if (previous_mount_info != NULL)
+ previous_free_space = (gdouble) previous_mount_info->buf.f_bavail / (gdouble) previous_mount_info->buf.f_blocks;
+
+ free_space = (gdouble) mount_info->buf.f_bavail / (gdouble) mount_info->buf.f_blocks;
+
+ if (previous_mount_info == NULL) {
+ /* We haven't notified for this mount yet */
+ show_notify = TRUE;
+ mount_info->notify_time = time (NULL);
+ g_hash_table_replace (ldsm_notified_hash, g_strdup (path), mount_info);
+ } else if ((previous_free_space - free_space) > free_percent_notify_again) {
+ /* We've notified for this mount before and free space has decreased sufficiently since last time to notify again */
+ curr_time = time (NULL);
+ if (difftime (curr_time, previous_mount_info->notify_time) > (gdouble)(min_notify_period * 60)) {
+ show_notify = TRUE;
+ mount_info->notify_time = curr_time;
+ } else {
+ /* It's too soon to show the dialog again. However, we still replace the LdsmMountInfo
+ * struct in the hash table, but give it the notfiy time from the previous dialog.
+ * This will stop the notification from reappearing unnecessarily as soon as the timeout expires.
+ */
+ show_notify = FALSE;
+ mount_info->notify_time = previous_mount_info->notify_time;
+ }
+ g_hash_table_replace (ldsm_notified_hash, g_strdup (path), mount_info);
+ } else {
+ /* We've notified for this mount before, but the free space hasn't decreased sufficiently to notify again */
+ ldsm_free_mount_info (mount_info);
+ show_notify = FALSE;
+ }
+
+ if (show_notify) {
+ ldsm_notify_for_mount (mount_info, multiple_volumes);
+ done = TRUE;
+ }
+ }
+}
+
+static gboolean
+ldsm_check_all_mounts (gpointer data)
+{
+ GList *mounts;
+ GList *l;
+ GList *check_mounts = NULL;
+ GList *full_mounts = NULL;
+ guint number_of_mounts = 0;
+ gboolean multiple_volumes = FALSE;
+
+ /* We iterate through the static mounts in /etc/fstab first, seeing if
+ * they're mounted by checking if the GUnixMountPoint has a corresponding GUnixMountEntry.
+ * Iterating through the static mounts means we automatically ignore dynamically mounted media.
+ */
+ mounts = g_unix_mount_points_get (time_read);
+
+ for (l = mounts; l != NULL; l = l->next) {
+ GUnixMountPoint *mount_point = l->data;
+ GUnixMountEntry *mount;
+ LdsmMountInfo *mount_info;
+ const gchar *path;
+
+ path = g_unix_mount_point_get_mount_path (mount_point);
+ mount = g_unix_mount_at (path, time_read);
+ g_unix_mount_point_free (mount_point);
+ if (mount == NULL) {
+ /* The GUnixMountPoint is not mounted */
+ continue;
+ }
+
+ mount_info = g_new0 (LdsmMountInfo, 1);
+ mount_info->mount = mount;
+
+ path = g_unix_mount_get_mount_path (mount);
+
+ if (g_unix_mount_is_readonly (mount)) {
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ if (ldsm_mount_is_user_ignore (path)) {
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ if (gsd_should_ignore_unix_mount (mount)) {
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ if (statvfs (path, &mount_info->buf) != 0) {
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ if (ldsm_mount_is_virtual (mount_info)) {
+ ldsm_free_mount_info (mount_info);
+ continue;
+ }
+
+ check_mounts = g_list_prepend (check_mounts, mount_info);
+ number_of_mounts += 1;
+ }
+
+ g_list_free (mounts);
+
+ if (number_of_mounts > 1)
+ multiple_volumes = TRUE;
+
+ for (l = check_mounts; l != NULL; l = l->next) {
+ LdsmMountInfo *mount_info = l->data;
+
+ if (!ldsm_mount_has_space (mount_info)) {
+ full_mounts = g_list_prepend (full_mounts, mount_info);
+ } else {
+ g_hash_table_remove (ldsm_notified_hash, g_unix_mount_get_mount_path (mount_info->mount));
+ ldsm_free_mount_info (mount_info);
+ }
+ }
+
+ ldsm_maybe_warn_mounts (full_mounts, multiple_volumes);
+
+ g_list_free (check_mounts);
+ g_list_free (full_mounts);
+
+ return TRUE;
+}
+
+static gboolean
+ldsm_is_hash_item_not_in_mounts (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ GList *l;
+
+ for (l = (GList *) user_data; l != NULL; l = l->next) {
+ GUnixMountEntry *mount = l->data;
+ const char *path;
+
+ path = g_unix_mount_get_mount_path (mount);
+
+ if (strcmp (path, key) == 0)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+ldsm_mounts_changed (GObject *monitor,
+ gpointer data)
+{
+ GList *mounts;
+
+ /* remove the saved data for mounts that got removed */
+ mounts = g_unix_mounts_get (time_read);
+ g_hash_table_foreach_remove (ldsm_notified_hash,
+ ldsm_is_hash_item_not_in_mounts, mounts);
+ g_list_free_full (mounts, (GDestroyNotify) g_unix_mount_free);
+
+ /* check the status now, for the new mounts */
+ ldsm_check_all_mounts (NULL);
+
+ /* and reset the timeout */
+ if (ldsm_timeout_id)
+ g_source_remove (ldsm_timeout_id);
+ ldsm_timeout_id = g_timeout_add_seconds (CHECK_EVERY_X_SECONDS,
+ ldsm_check_all_mounts, NULL);
+ g_source_set_name_by_id (ldsm_timeout_id, "[gnome-settings-daemon] ldsm_check_all_mounts");
+}
+
+static gboolean
+ldsm_is_hash_item_in_ignore_paths (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ return ldsm_mount_is_user_ignore (key);
+}
+
+static void
+gsd_ldsm_get_config (void)
+{
+ gchar **settings_list;
+
+ free_percent_notify = g_settings_get_double (settings, SETTINGS_FREE_PC_NOTIFY_KEY);
+ free_percent_notify_again = g_settings_get_double (settings, SETTINGS_FREE_PC_NOTIFY_AGAIN_KEY);
+
+ free_size_gb_no_notify = g_settings_get_int (settings, SETTINGS_FREE_SIZE_NO_NOTIFY);
+ min_notify_period = g_settings_get_int (settings, SETTINGS_MIN_NOTIFY_PERIOD);
+
+ if (ignore_paths != NULL) {
+ g_slist_foreach (ignore_paths, (GFunc) g_free, NULL);
+ g_clear_pointer (&ignore_paths, g_slist_free);
+ }
+
+ settings_list = g_settings_get_strv (settings, SETTINGS_IGNORE_PATHS);
+ if (settings_list != NULL) {
+ guint i;
+
+ for (i = 0; settings_list[i] != NULL; i++)
+ ignore_paths = g_slist_prepend (ignore_paths, g_strdup (settings_list[i]));
+
+ /* Make sure we dont leave stale entries in ldsm_notified_hash */
+ g_hash_table_foreach_remove (ldsm_notified_hash,
+ ldsm_is_hash_item_in_ignore_paths, NULL);
+
+ g_strfreev (settings_list);
+ }
+
+ purge_trash = g_settings_get_boolean (privacy_settings, SETTINGS_PURGE_TRASH);
+ purge_temp_files = g_settings_get_boolean (privacy_settings, SETTINGS_PURGE_TEMP_FILES);
+ purge_after = g_settings_get_uint (privacy_settings, SETTINGS_PURGE_AFTER);
+}
+
+static void
+gsd_ldsm_update_config (GSettings *settings,
+ const gchar *key,
+ gpointer user_data)
+{
+ gsd_ldsm_get_config ();
+}
+
+void
+gsd_ldsm_setup (gboolean check_now)
+{
+ if (ldsm_notified_hash || ldsm_timeout_id || ldsm_monitor) {
+ g_warning ("Low disk space monitor already initialized.");
+ return;
+ }
+
+ ldsm_notified_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free,
+ ldsm_free_mount_info);
+
+ settings = g_settings_new (SETTINGS_HOUSEKEEPING_DIR);
+ privacy_settings = g_settings_new (PRIVACY_SETTINGS);
+ gsd_ldsm_get_config ();
+ g_signal_connect (G_OBJECT (settings), "changed",
+ G_CALLBACK (gsd_ldsm_update_config), NULL);
+
+ ldsm_monitor = g_unix_mount_monitor_get ();
+ g_signal_connect (ldsm_monitor, "mounts-changed",
+ G_CALLBACK (ldsm_mounts_changed), NULL);
+
+ if (check_now)
+ ldsm_check_all_mounts (NULL);
+
+ ldsm_timeout_id = g_timeout_add_seconds (CHECK_EVERY_X_SECONDS,
+ ldsm_check_all_mounts, NULL);
+ g_source_set_name_by_id (ldsm_timeout_id, "[gnome-settings-daemon] ldsm_check_all_mounts");
+
+ purge_trash_id = g_timeout_add_seconds (3600, ldsm_purge_trash_and_temp, NULL);
+ g_source_set_name_by_id (purge_trash_id, "[gnome-settings-daemon] ldsm_purge_trash_and_temp");
+}
+
+void
+gsd_ldsm_clean (void)
+{
+ if (purge_trash_id)
+ g_source_remove (purge_trash_id);
+ purge_trash_id = 0;
+
+ if (purge_temp_id)
+ g_source_remove (purge_temp_id);
+ purge_temp_id = 0;
+
+ if (ldsm_timeout_id)
+ g_source_remove (ldsm_timeout_id);
+ ldsm_timeout_id = 0;
+
+ g_clear_pointer (&ldsm_notified_hash, g_hash_table_destroy);
+ g_clear_object (&ldsm_monitor);
+ g_clear_object (&settings);
+ g_clear_object (&privacy_settings);
+ /* NotifyNotification::closed callback will drop reference */
+ if (notification != NULL)
+ notify_notification_close (notification, NULL);
+ g_slist_free_full (ignore_paths, g_free);
+ ignore_paths = NULL;
+}
+
diff --git a/plugins/housekeeping/gsd-disk-space.h b/plugins/housekeeping/gsd-disk-space.h
new file mode 100644
index 0000000..d90d36e
--- /dev/null
+++ b/plugins/housekeeping/gsd-disk-space.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_DISK_SPACE_H
+#define __GSD_DISK_SPACE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct {
+ grefcount ref_count;
+ GFile *file;
+ GCancellable *cancellable;
+ GDateTime *old;
+ gboolean dry_run;
+ gboolean trash;
+ gchar *name;
+ gchar *filesystem;
+ gint depth;
+} DeleteData;
+
+void delete_data_unref (DeleteData *data);
+DeleteData *delete_data_new (GFile *file,
+ GCancellable *cancellable,
+ GDateTime *old,
+ gboolean dry_run,
+ gboolean trash,
+ gint depth,
+ const char *filesystem);
+char* get_filesystem (GFile *file);
+
+void delete_recursively_by_age (DeleteData *data);
+
+void gsd_ldsm_setup (gboolean check_now);
+void gsd_ldsm_clean (void);
+
+/* for the test */
+void gsd_ldsm_show_empty_trash (void);
+void gsd_ldsm_purge_trash (GDateTime *old);
+void gsd_ldsm_purge_temp_files (GDateTime *old);
+
+G_END_DECLS
+
+#endif /* __GSD_DISK_SPACE_H */
diff --git a/plugins/housekeeping/gsd-empty-trash-test.c b/plugins/housekeeping/gsd-empty-trash-test.c
new file mode 100644
index 0000000..6a2c37f
--- /dev/null
+++ b/plugins/housekeeping/gsd-empty-trash-test.c
@@ -0,0 +1,44 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2011, Red Hat, Inc.
+ *
+ * Authors: Cosimo Cecchi <cosimoc@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include "gsd-disk-space.h"
+
+int
+main (int argc,
+ char **argv)
+{
+ GMainLoop *loop;
+
+ gtk_init (&argc, &argv);
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ gsd_ldsm_show_empty_trash ();
+ g_main_loop_run (loop);
+
+ g_main_loop_unref (loop);
+
+ return 0;
+}
+
diff --git a/plugins/housekeeping/gsd-housekeeping-manager.c b/plugins/housekeeping/gsd-housekeeping-manager.c
new file mode 100644
index 0000000..b291667
--- /dev/null
+++ b/plugins/housekeeping/gsd-housekeeping-manager.c
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2008 Michael J. Chudobiak <mjc@avtechpulse.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib/gstdio.h>
+#include <string.h>
+#include <libnotify/notify.h>
+
+#include "gnome-settings-profile.h"
+#include "gsd-housekeeping-manager.h"
+#include "gsd-disk-space.h"
+#include "gsd-systemd-notify.h"
+
+
+/* General */
+#define INTERVAL_ONCE_A_DAY 24*60*60
+#define INTERVAL_TWO_MINUTES 2*60
+
+/* Thumbnail cleaner */
+#define THUMB_PREFIX "org.gnome.desktop.thumbnail-cache"
+
+#define THUMB_AGE_KEY "maximum-age"
+#define THUMB_SIZE_KEY "maximum-size"
+
+#define GSD_HOUSEKEEPING_DBUS_PATH "/org/gnome/SettingsDaemon/Housekeeping"
+
+static const gchar introspection_xml[] =
+"<node>"
+" <interface name='org.gnome.SettingsDaemon.Housekeeping'>"
+" <method name='EmptyTrash'/>"
+" <method name='RemoveTempFiles'/>"
+" </interface>"
+"</node>";
+
+struct _GsdHousekeepingManager {
+ GObject parent;
+
+ GSettings *settings;
+ guint long_term_cb;
+ guint short_term_cb;
+
+ GDBusNodeInfo *introspection_data;
+ GDBusConnection *connection;
+ GCancellable *bus_cancellable;
+ guint name_id;
+
+ GsdSystemdNotify *systemd_notify;
+};
+
+static void gsd_housekeeping_manager_class_init (GsdHousekeepingManagerClass *klass);
+static void gsd_housekeeping_manager_init (GsdHousekeepingManager *housekeeping_manager);
+
+G_DEFINE_TYPE (GsdHousekeepingManager, gsd_housekeeping_manager, G_TYPE_OBJECT)
+
+static gpointer manager_object = NULL;
+
+
+typedef struct {
+ glong now;
+ glong max_age;
+ goffset total_size;
+ goffset max_size;
+} PurgeData;
+
+
+typedef struct {
+ gint64 time;
+ char *path;
+ glong size;
+} ThumbData;
+
+
+static void
+thumb_data_free (gpointer data)
+{
+ ThumbData *info = data;
+
+ if (info) {
+ g_free (info->path);
+ g_free (info);
+ }
+}
+
+static GList *
+read_dir_for_purge (const char *path, GList *files)
+{
+ GFile *read_path;
+ GFileEnumerator *enum_dir;
+ int cannot_get_time = 0;
+
+ read_path = g_file_new_for_path (path);
+ enum_dir = g_file_enumerate_children (read_path,
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_TIME_ACCESS ","
+ G_FILE_ATTRIBUTE_TIME_MODIFIED ","
+ G_FILE_ATTRIBUTE_STANDARD_SIZE,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL);
+
+ if (enum_dir != NULL) {
+ GFileInfo *info;
+ while ((info = g_file_enumerator_next_file (enum_dir, NULL, NULL)) != NULL) {
+ const char *name;
+ name = g_file_info_get_name (info);
+
+ if (strlen (name) == 36 && strcmp (name + 32, ".png") == 0) {
+ ThumbData *td;
+ GFile *entry;
+ char *entry_path;
+ gint64 time;
+
+ entry = g_file_get_child (read_path, name);
+ entry_path = g_file_get_path (entry);
+ g_object_unref (entry);
+
+ // If atime is available, it should be no worse than mtime.
+ // - Even if the file system is mounted with noatime, the atime and
+ // mtime will be set to the same value on file creation.
+ // - Since the thumbnailer never edits thumbnails, and instead swaps
+ // in newly created temp files, atime will always be >= mtime.
+
+ if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_ACCESS)) {
+ time = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS);
+ } else if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED)) {
+ time = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ } else {
+ // Unlikely to get here, but be defensive
+ cannot_get_time += 1;
+ time = G_MAXINT64;
+ }
+
+ td = g_new0 (ThumbData, 1);
+ td->path = entry_path;
+ td->time = time;
+ td->size = g_file_info_get_size (info);
+
+ files = g_list_prepend (files, td);
+ }
+ g_object_unref (info);
+ }
+ g_object_unref (enum_dir);
+
+ if (cannot_get_time > 0) {
+ g_warning ("Could not read atime or mtime on %d files in %s", cannot_get_time, path);
+ }
+ }
+ g_object_unref (read_path);
+
+ return files;
+}
+
+static void
+purge_old_thumbnails (ThumbData *info, PurgeData *purge_data)
+{
+ if ((purge_data->now - info->time) > purge_data->max_age) {
+ g_unlink (info->path);
+ info->size = 0;
+ } else {
+ purge_data->total_size += info->size;
+ }
+}
+
+static int
+sort_file_time (ThumbData *file1, ThumbData *file2)
+{
+ return file1->time - file2->time;
+}
+
+static char **
+get_thumbnail_dirs (void)
+{
+ GPtrArray *array;
+ char *path;
+
+ array = g_ptr_array_new ();
+
+ /* check new XDG cache */
+ path = g_build_filename (g_get_user_cache_dir (),
+ "thumbnails",
+ "normal",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ path = g_build_filename (g_get_user_cache_dir (),
+ "thumbnails",
+ "large",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ path = g_build_filename (g_get_user_cache_dir (),
+ "thumbnails",
+ "fail",
+ "gnome-thumbnail-factory",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ /* cleanup obsolete locations too */
+ path = g_build_filename (g_get_home_dir (),
+ ".thumbnails",
+ "normal",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ path = g_build_filename (g_get_home_dir (),
+ ".thumbnails",
+ "large",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ path = g_build_filename (g_get_home_dir (),
+ ".thumbnails",
+ "fail",
+ "gnome-thumbnail-factory",
+ NULL);
+ g_ptr_array_add (array, path);
+
+ g_ptr_array_add (array, NULL);
+
+ return (char **) g_ptr_array_free (array, FALSE);
+}
+
+static void
+purge_thumbnail_cache (GsdHousekeepingManager *manager)
+{
+
+ char **paths;
+ GList *files;
+ PurgeData purge_data;
+ GTimeVal current_time;
+ guint i;
+
+ g_debug ("housekeeping: checking thumbnail cache size and freshness");
+
+ purge_data.max_age = (glong) g_settings_get_int (manager->settings, THUMB_AGE_KEY) * 24 * 60 * 60;
+ purge_data.max_size = (goffset) g_settings_get_int (manager->settings, THUMB_SIZE_KEY) * 1024 * 1024;
+
+ /* if both are set to -1, we don't need to read anything */
+ if ((purge_data.max_age < 0) && (purge_data.max_size < 0))
+ return;
+
+ paths = get_thumbnail_dirs ();
+ files = NULL;
+ for (i = 0; paths[i] != NULL; i++)
+ files = read_dir_for_purge (paths[i], files);
+ g_strfreev (paths);
+
+ g_get_current_time (&current_time);
+
+ purge_data.now = current_time.tv_sec;
+ purge_data.total_size = 0;
+
+ if (purge_data.max_age >= 0)
+ g_list_foreach (files, (GFunc) purge_old_thumbnails, &purge_data);
+
+ if ((purge_data.total_size > purge_data.max_size) && (purge_data.max_size >= 0)) {
+ GList *scan;
+ files = g_list_sort (files, (GCompareFunc) sort_file_time);
+ for (scan = files; scan && (purge_data.total_size > purge_data.max_size); scan = scan->next) {
+ ThumbData *info = scan->data;
+ g_unlink (info->path);
+ purge_data.total_size -= info->size;
+ }
+ }
+
+ g_list_foreach (files, (GFunc) thumb_data_free, NULL);
+ g_list_free (files);
+}
+
+static gboolean
+do_cleanup (GsdHousekeepingManager *manager)
+{
+ purge_thumbnail_cache (manager);
+ return TRUE;
+}
+
+static gboolean
+do_cleanup_once (GsdHousekeepingManager *manager)
+{
+ do_cleanup (manager);
+ manager->short_term_cb = 0;
+ return FALSE;
+}
+
+static void
+do_cleanup_soon (GsdHousekeepingManager *manager)
+{
+ if (manager->short_term_cb == 0) {
+ g_debug ("housekeeping: will tidy up in 2 minutes");
+ manager->short_term_cb = g_timeout_add_seconds (INTERVAL_TWO_MINUTES,
+ (GSourceFunc) do_cleanup_once,
+ manager);
+ g_source_set_name_by_id (manager->short_term_cb, "[gnome-settings-daemon] do_cleanup_once");
+ }
+}
+
+static void
+settings_changed_callback (GSettings *settings,
+ const char *key,
+ GsdHousekeepingManager *manager)
+{
+ do_cleanup_soon (manager);
+}
+
+static void
+handle_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ GDateTime *now;
+ now = g_date_time_new_now_local ();
+ if (g_strcmp0 (method_name, "EmptyTrash") == 0) {
+ gsd_ldsm_purge_trash (now);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else if (g_strcmp0 (method_name, "RemoveTempFiles") == 0) {
+ gsd_ldsm_purge_temp_files (now);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ g_date_time_unref (now);
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+ handle_method_call,
+ NULL, /* Get Property */
+ NULL, /* Set Property */
+};
+
+static void
+on_bus_gotten (GObject *source_object,
+ GAsyncResult *res,
+ GsdHousekeepingManager *manager)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+ GDBusInterfaceInfo **infos;
+ int i;
+
+ connection = g_bus_get_finish (res, &error);
+ if (connection == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Could not get session bus: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+ manager->connection = connection;
+
+ infos = manager->introspection_data->interfaces;
+ for (i = 0; infos[i] != NULL; i++) {
+ g_dbus_connection_register_object (connection,
+ GSD_HOUSEKEEPING_DBUS_PATH,
+ infos[i],
+ &interface_vtable,
+ manager,
+ NULL,
+ NULL);
+ }
+
+ manager->name_id = g_bus_own_name_on_connection (connection,
+ "org.gnome.SettingsDaemon.Housekeeping",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ NULL,
+ NULL,
+ NULL,
+ NULL);
+}
+
+static void
+register_manager_dbus (GsdHousekeepingManager *manager)
+{
+ manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
+ g_assert (manager->introspection_data != NULL);
+ manager->bus_cancellable = g_cancellable_new ();
+
+ g_bus_get (G_BUS_TYPE_SESSION,
+ manager->bus_cancellable,
+ (GAsyncReadyCallback) on_bus_gotten,
+ manager);
+}
+
+gboolean
+gsd_housekeeping_manager_start (GsdHousekeepingManager *manager,
+ GError **error)
+{
+ gchar *dir;
+
+ g_debug ("Starting housekeeping manager");
+ gnome_settings_profile_start (NULL);
+
+ /* Create ~/.local/ as early as possible */
+ (void) g_mkdir_with_parents(g_get_user_data_dir (), 0700);
+
+ /* Create ~/.local/share/applications/, see
+ * https://bugzilla.gnome.org/show_bug.cgi?id=703048 */
+ dir = g_build_filename (g_get_user_data_dir (), "applications", NULL);
+ (void) g_mkdir (dir, 0700);
+ g_free (dir);
+
+ gsd_ldsm_setup (FALSE);
+
+ manager->settings = g_settings_new (THUMB_PREFIX);
+ g_signal_connect (G_OBJECT (manager->settings), "changed",
+ G_CALLBACK (settings_changed_callback), manager);
+
+ /* Clean once, a few minutes after start-up */
+ do_cleanup_soon (manager);
+
+ /* Clean periodically, on a daily basis. */
+ manager->long_term_cb = g_timeout_add_seconds (INTERVAL_ONCE_A_DAY,
+ (GSourceFunc) do_cleanup,
+ manager);
+ g_source_set_name_by_id (manager->long_term_cb, "[gnome-settings-daemon] do_cleanup");
+
+ manager->systemd_notify = g_object_new (GSD_TYPE_SYSTEMD_NOTIFY, NULL);
+
+ gnome_settings_profile_end (NULL);
+
+ return TRUE;
+}
+
+void
+gsd_housekeeping_manager_stop (GsdHousekeepingManager *manager)
+{
+ g_debug ("Stopping housekeeping manager");
+
+ if (manager->name_id != 0) {
+ g_bus_unown_name (manager->name_id);
+ manager->name_id = 0;
+ }
+
+ g_clear_object (&manager->bus_cancellable);
+ g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref);
+ g_clear_object (&manager->connection);
+
+ g_clear_object (&manager->systemd_notify);
+
+ if (manager->short_term_cb) {
+ g_source_remove (manager->short_term_cb);
+ manager->short_term_cb = 0;
+ }
+
+ if (manager->long_term_cb) {
+ g_source_remove (manager->long_term_cb);
+ manager->long_term_cb = 0;
+
+ /* Do a clean-up on shutdown if and only if the size or age
+ limits have been set to paranoid levels (zero) */
+ if ((g_settings_get_int (manager->settings, THUMB_AGE_KEY) == 0) ||
+ (g_settings_get_int (manager->settings, THUMB_SIZE_KEY) == 0)) {
+ do_cleanup (manager);
+ }
+
+ }
+
+ g_clear_object (&manager->settings);
+ gsd_ldsm_clean ();
+}
+
+static void
+gsd_housekeeping_manager_finalize (GObject *object)
+{
+ gsd_housekeeping_manager_stop (GSD_HOUSEKEEPING_MANAGER (object));
+
+ G_OBJECT_CLASS (gsd_housekeeping_manager_parent_class)->finalize (object);
+}
+
+static void
+gsd_housekeeping_manager_class_init (GsdHousekeepingManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_housekeeping_manager_finalize;
+
+ notify_init ("gnome-settings-daemon");
+}
+
+static void
+gsd_housekeeping_manager_init (GsdHousekeepingManager *manager)
+{
+}
+
+GsdHousekeepingManager *
+gsd_housekeeping_manager_new (void)
+{
+ if (manager_object != NULL) {
+ g_object_ref (manager_object);
+ } else {
+ manager_object = g_object_new (GSD_TYPE_HOUSEKEEPING_MANAGER, NULL);
+ g_object_add_weak_pointer (manager_object,
+ (gpointer *) &manager_object);
+
+ register_manager_dbus (manager_object);
+ }
+
+ return GSD_HOUSEKEEPING_MANAGER (manager_object);
+}
diff --git a/plugins/housekeeping/gsd-housekeeping-manager.h b/plugins/housekeeping/gsd-housekeeping-manager.h
new file mode 100644
index 0000000..8b5f840
--- /dev/null
+++ b/plugins/housekeeping/gsd-housekeeping-manager.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Michael J. Chudobiak <mjc@avtechpulse.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_HOUSEKEEPING_MANAGER_H
+#define __GSD_HOUSEKEEPING_MANAGER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_HOUSEKEEPING_MANAGER (gsd_housekeeping_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsdHousekeepingManager, gsd_housekeeping_manager, GSD, HOUSEKEEPING_MANAGER, GObject)
+
+GsdHousekeepingManager * gsd_housekeeping_manager_new (void);
+gboolean gsd_housekeeping_manager_start (GsdHousekeepingManager *manager,
+ GError **error);
+void gsd_housekeeping_manager_stop (GsdHousekeepingManager *manager);
+
+G_END_DECLS
+
+#endif /* __GSD_HOUSEKEEPING_MANAGER_H */
diff --git a/plugins/housekeeping/gsd-purge-temp-test.c b/plugins/housekeeping/gsd-purge-temp-test.c
new file mode 100644
index 0000000..6ff2d5e
--- /dev/null
+++ b/plugins/housekeeping/gsd-purge-temp-test.c
@@ -0,0 +1,62 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ * vim: set et sw=8 ts=8:
+ *
+ * Copyright (c) 2008, Novell, Inc.
+ *
+ * Authors: Vincent Untz <vuntz@gnome.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include <libnotify/notify.h>
+#include "gsd-disk-space.h"
+
+int
+main (int argc,
+ char **argv)
+{
+ GFile *file;
+ DeleteData *data;
+ GDateTime *old;
+ GMainLoop *loop;
+ g_autofree char *filesystem = NULL;
+
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+
+ gtk_init (&argc, &argv);
+ notify_init ("gsd-purge-temp-test");
+ loop = g_main_loop_new (NULL, FALSE);
+
+ file = g_file_new_for_path ("/tmp/gsd-purge-temp-test");
+ if (!g_file_query_exists (file, NULL)) {
+ g_warning ("Create /tmp/gsd-purge-temp-test and add some files to it to test deletion by date");
+ g_object_unref (file);
+ return 1;
+ }
+
+ old = g_date_time_new_now_local ();
+ filesystem = get_filesystem (file);
+ data = delete_data_new (file, NULL, old, FALSE, FALSE, 0, filesystem);
+ delete_recursively_by_age (data);
+ delete_data_unref (data);
+ g_object_unref (file);
+
+ g_main_loop_run (loop);
+
+ return 0;
+}
+
diff --git a/plugins/housekeeping/gsd-systemd-notify.c b/plugins/housekeeping/gsd-systemd-notify.c
new file mode 100644
index 0000000..9ead905
--- /dev/null
+++ b/plugins/housekeeping/gsd-systemd-notify.c
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 Benjamin Berg <bberg@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <string.h>
+
+#include "gsd-systemd-notify.h"
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gio/gdesktopappinfo.h>
+#include <libnotify/notify.h>
+
+struct _GsdSystemdNotify {
+ GObject parent;
+
+ GDBusConnection *session;
+ guint sub_service;
+ guint sub_scope;
+};
+
+G_DEFINE_TYPE (GsdSystemdNotify, gsd_systemd_notify, G_TYPE_OBJECT)
+
+static void
+notify_oom_kill (char *unit)
+{
+ g_autoptr(GDesktopAppInfo) app = NULL;
+ g_autofree char *unit_copy = NULL;
+ g_autofree char *app_id = NULL;
+ g_autofree char *desktop_id = NULL;
+ g_autofree char *summary = NULL;
+ g_autofree char *message = NULL;
+ NotifyNotification *notification = NULL;
+ char *pos;
+
+ unit_copy = g_strdup (unit);
+
+ if (g_str_has_suffix (unit_copy, ".service")) {
+ /* Find (first) @ character */
+ pos = strchr (unit_copy, '@');
+ if (pos)
+ *pos = '\0';
+ } else if (g_str_has_suffix (unit_copy, ".scope")) {
+ /* Find last - character */
+ pos = strrchr (unit_copy, '-');
+ if (pos)
+ *pos = '\0';
+ } else {
+ /* This cannot happen, because we only subscribe to the Scope
+ * and Service DBus interfaces.
+ */
+ g_assert_not_reached ();
+ return;
+ }
+
+
+ pos = strrchr (unit_copy, '-');
+ if (pos) {
+ pos += 1;
+
+ app_id = g_strcompress (pos);
+ desktop_id = g_strjoin (NULL, app_id, ".desktop", NULL);
+
+ app = g_desktop_app_info_new (desktop_id);
+ }
+
+ if (app) {
+ /* TRANSLATORS: %s is the application name. */
+ summary = g_strdup_printf (_("%s Stopped"),
+ g_app_info_get_name (G_APP_INFO (app)));
+ /* TRANSLATORS: %s is the application name. */
+ message = g_strdup_printf (_("Device memory is nearly full. %s was using a lot of memory and was forced to stop."),
+ g_app_info_get_name (G_APP_INFO (app)));
+ } else if (g_str_has_prefix (unit, "vte-spawn-")) {
+ /* TRANSLATORS: A terminal tab/window was killed. */
+ summary = g_strdup_printf (_("Virtual Terminal Stopped"));
+ /* TRANSLATORS: A terminal tab/window was killed. */
+ message = g_strdup_printf (_("Device memory is nearly full. Virtual Terminal processes were using a lot of memory and were forced to stop."));
+ } else {
+ /* TRANSLATORS: We don't have a good description of what was killed. */
+ summary = g_strdup_printf (_("Application Stopped"));
+ /* TRANSLATORS: We don't have a good description of what was killed. */
+ message = g_strdup_printf (_("Device memory is nearly full. An Application that was using a lot of memory and was forced to stop."));
+ }
+
+ notification = notify_notification_new (summary, message, "dialog-warning-symbolic");
+
+ if (app) {
+ notify_notification_set_hint_string (notification, "desktop-entry", desktop_id);
+ notify_notification_set_app_name (notification, g_app_info_get_name (G_APP_INFO (app)));
+ }
+ notify_notification_set_hint (notification, "transient", g_variant_new_boolean (TRUE));
+ notify_notification_set_urgency (notification, NOTIFY_URGENCY_CRITICAL);
+ notify_notification_set_timeout (notification, NOTIFY_EXPIRES_DEFAULT);
+
+ notify_notification_show (notification, NULL);
+ g_object_unref (notification);
+}
+
+/* Taken from hexdecoct.c in systemd, LGPL-2.1-or-later */
+static int
+unhexchar (char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+
+ return -EINVAL;
+}
+
+
+static char*
+unescape_dbus_path (const char *path)
+{
+ g_autofree char *res = g_malloc (strlen (path) + 1);
+ char *r;
+
+ for (r = res; *path; path += 1, r += 1) {
+ int c1, c2;
+ if (*path != '_') {
+ *r = *path;
+ continue;
+ }
+ /* Read next two hex characters */
+ path += 1;
+ c1 = unhexchar (*path);
+ if (c1 < 0)
+ return NULL;
+ path += 1;
+ c2 = unhexchar (*path);
+ if (c2 < 0)
+ return NULL;
+
+ *r = (c1 << 4) | c2;
+ }
+ *r = '\0';
+
+ return g_steal_pointer (&res);
+}
+
+static void
+on_unit_properties_changed (GDBusConnection *connection,
+ const char *sender_name,
+ const char *object_path,
+ const char *interface_name,
+ const char *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ g_autoptr(GVariant) dict = NULL;
+ const char *result = NULL;
+ const char *unit_escaped = NULL;
+ g_autofree char *unit = NULL;
+
+ g_assert (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sa{sv}as)")));
+
+ dict = g_variant_get_child_value (parameters, 1);
+ g_assert (dict);
+
+ unit_escaped = strrchr (object_path, '/');
+ g_assert (unit_escaped);
+ unit_escaped += 1;
+
+ unit = unescape_dbus_path (unit_escaped);
+ g_assert (unit);
+
+ if (g_variant_lookup (dict, "Result", "&s", &result)) {
+ if (g_strcmp0 (result, "oom-kill") == 0)
+ notify_oom_kill (unit);
+ }
+}
+
+static void
+on_bus_gotten (GDBusConnection *obj,
+ GAsyncResult *res,
+ GsdSystemdNotify *self)
+{
+ g_autoptr(GError) error = NULL;
+ GDBusConnection *con;
+
+ con = g_bus_get_finish (res, &error);
+ if (!con) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to get session bus: %s", error->message);
+ return;
+ }
+
+ self->session = con;
+ self->sub_service = g_dbus_connection_signal_subscribe (self->session,
+ "org.freedesktop.systemd1",
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ NULL,
+ "org.freedesktop.systemd1.Service",
+ G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE,
+ on_unit_properties_changed,
+ self,
+ NULL);
+
+ self->sub_scope = g_dbus_connection_signal_subscribe (self->session,
+ "org.freedesktop.systemd1",
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ NULL,
+ "org.freedesktop.systemd1.Scope",
+ G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE,
+ on_unit_properties_changed,
+ self,
+ NULL);
+}
+
+static void
+gsd_systemd_notify_init (GsdSystemdNotify *self)
+{
+ g_bus_get (G_BUS_TYPE_SESSION, NULL, (GAsyncReadyCallback) on_bus_gotten, self);
+}
+
+static void
+gsd_systemd_notify_dispose (GObject *obj)
+{
+ GsdSystemdNotify *self = GSD_SYSTEMD_NOTIFY (obj);
+
+ if (self->sub_service) {
+ g_dbus_connection_signal_unsubscribe (self->session, self->sub_service);
+ g_dbus_connection_signal_unsubscribe (self->session, self->sub_scope);
+ }
+ self->sub_service = 0;
+ self->sub_scope = 0;
+ g_clear_object (&self->session);
+
+ G_OBJECT_CLASS (gsd_systemd_notify_parent_class)->dispose (obj);
+}
+
+static void
+gsd_systemd_notify_class_init (GsdSystemdNotifyClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gsd_systemd_notify_dispose;
+}
+
diff --git a/plugins/housekeeping/gsd-systemd-notify.h b/plugins/housekeeping/gsd-systemd-notify.h
new file mode 100644
index 0000000..51560d1
--- /dev/null
+++ b/plugins/housekeeping/gsd-systemd-notify.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 Benjamin Berg <bberg@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_SYSTEMD_NOTIFY_H
+#define __GSD_SYSTEMD_NOTIFY_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_SYSTEMD_NOTIFY (gsd_systemd_notify_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsdSystemdNotify, gsd_systemd_notify, GSD, SYSTEMD_NOTIFY, GObject)
+
+G_END_DECLS
+
+#endif /* __GSD_SYSTEMD_NOTIFY_H */
diff --git a/plugins/housekeeping/main.c b/plugins/housekeeping/main.c
new file mode 100644
index 0000000..89c12f7
--- /dev/null
+++ b/plugins/housekeeping/main.c
@@ -0,0 +1,7 @@
+#define NEW gsd_housekeeping_manager_new
+#define START gsd_housekeeping_manager_start
+#define STOP gsd_housekeeping_manager_stop
+#define MANAGER GsdHousekeepingManager
+#include "gsd-housekeeping-manager.h"
+
+#include "daemon-skeleton.h"
diff --git a/plugins/housekeeping/meson.build b/plugins/housekeeping/meson.build
new file mode 100644
index 0000000..ce7ba81
--- /dev/null
+++ b/plugins/housekeeping/meson.build
@@ -0,0 +1,42 @@
+common_files = files(
+ 'gsd-disk-space.c',
+ 'gsd-disk-space-helper.c'
+)
+
+sources = common_files + files(
+ 'gsd-housekeeping-manager.c',
+ 'gsd-systemd-notify.c',
+ 'main.c'
+)
+
+deps = plugins_deps + [
+ gio_unix_dep,
+ gtk_dep,
+ libnotify_dep
+]
+
+executable(
+ 'gsd-' + plugin_name,
+ sources,
+ include_directories: [top_inc, common_inc],
+ dependencies: deps,
+ c_args: cflags,
+ install: true,
+ install_rpath: gsd_pkglibdir,
+ install_dir: gsd_libexecdir
+)
+
+programs = [
+ 'gsd-disk-space-test',
+ 'gsd-empty-trash-test',
+ 'gsd-purge-temp-test'
+]
+
+foreach program: programs
+ executable(
+ program,
+ common_files + [program + '.c'],
+ include_directories: top_inc,
+ dependencies: deps
+ )
+endforeach