gvfs/daemon/trashlib/trashwatcher.c

393 lines
10 KiB
C

/*
* Copyright © 2008 Ryan Lortie
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of version 3 of the GNU General Public License as
* published by the Free Software Foundation.
*/
#include "trashwatcher.h"
#include <gio/gunixmounts.h>
#include <gio/gio.h>
#include <unistd.h>
#include <string.h>
#include "trashitem.h"
#include "trashdir.h"
typedef enum
{
TRASH_WATCHER_TRUSTED,
TRASH_WATCHER_WATCH,
TRASH_WATCHER_NO_WATCH,
} WatchType;
/* decide_watch_type:
*
* This function is responsible for determining what sort of watching
* we should do on a given mountpoint according to the type of
* filesystem. It must return one of the WatchType constants above.
*
* TRASH_WATCHER_TRUSTED:
*
* This is used for filesystems on which notification is supported
* and all file events are reliably reported. After initialisation
* the trash directories are never manually rescanned since any
* changes are already known to us from the notifications we
* received about them (that's where the "trust" comes in).
*
* This should be used for local filesystems such as ext3.
*
* TRASH_WATCHER_WATCH:
*
* This is used for filesystems on which notification is supported
* but works unreliably. Some changes to the filesystem may not
* be delivered by the operating system. The events which are
* delivered are immediately reported but events which are not
* delivered are not reported until the directory is manually
* rescanned (ie: trash_watcher_rescan() is called).
*
* This should be used for filesystems like NFS where local
* changes are reported by the kernel but changes made on other
* hosts are not.
*
* TRASH_WATCHER_NO_WATCH:
*
* Don't bother watching at all. No change events are ever
* delivered except while running trash_watcher_rescan().
*
* This should be used for filesystems where change notification
* is unsupported or is supported, but buggy enough to cause
* problems when using the other two options.
*/
static WatchType
decide_watch_type (GUnixMountEntry *mount,
gboolean is_home_trash)
{
const gchar *fs_type;
const gchar *mount_path;
/* Let's assume that home trash is trusted if mount wasn't found.
* https://bugzilla.gnome.org/show_bug.cgi?id=747540
*/
if (mount == NULL)
return TRASH_WATCHER_TRUSTED;
mount_path = g_unix_mount_get_mount_path (mount);
/* Do not care about mount points without read access to avoid polling, see:
* https://bugzilla.gnome.org/show_bug.cgi?id=522314
*/
if (access (mount_path, R_OK) != 0)
return TRASH_WATCHER_NO_WATCH;
fs_type = g_unix_mount_get_fs_type (mount);
if (strcmp (fs_type, "nfs") == 0)
return TRASH_WATCHER_WATCH;
else
return TRASH_WATCHER_TRUSTED;
}
/* find the mount entry for the directory containing 'file'.
* used to figure out what sort of filesystem the home trash
* folder is sitting on.
*/
static GUnixMountEntry *
find_mount_entry_for_file (GFile *file)
{
GUnixMountEntry *entry;
char *pathname;
pathname = g_file_get_path (file);
do
{
char *slash;
slash = strrchr (pathname, '/');
/* leave the leading '/' in place */
if (slash == pathname)
slash++;
*slash = '\0';
entry = g_unix_mount_for (pathname, NULL);
}
while (entry == NULL && pathname[1]);
g_free (pathname);
/* Entry might not be found e.g. for bind mounts, btrfs subvolumes...
* https://bugzilla.gnome.org/show_bug.cgi?id=747540
*/
if (entry == NULL)
{
pathname = g_file_get_path (file);
g_warning ("Mount entry was not found for %s", pathname);
g_free (pathname);
}
return entry;
}
typedef struct _TrashMount TrashMount;
struct OPAQUE_TYPE__TrashWatcher
{
TrashRoot *root;
GUnixMountMonitor *mount_monitor;
TrashMount *mounts;
TrashDir *homedir_trashdir;
WatchType homedir_type;
gboolean watching;
};
struct _TrashMount
{
GUnixMountEntry *mount_entry;
TrashDir *dirs[2];
WatchType type;
TrashMount *next;
};
static void
trash_mount_insert (TrashWatcher *watcher,
TrashMount ***mount_ptr_ptr,
GUnixMountEntry *mount_entry)
{
const char *mountpoint;
gboolean watching;
TrashMount *mount;
mountpoint = g_unix_mount_get_mount_path (mount_entry);
mount = g_slice_new (TrashMount);
mount->mount_entry = mount_entry;
mount->type = decide_watch_type (mount_entry, FALSE);
watching = watcher->watching && mount->type != TRASH_WATCHER_NO_WATCH;
/* """
* For showing trashed files, implementations SHOULD support (1) and
* (2) at the same time (i.e. if both $topdir/.Trash/$uid and
* $topdir/.Trash-$uid are present, it should list trashed files
* from both of them).
* """
*/
/* (1) */
mount->dirs[0] = trash_dir_new (watcher->root, watching, FALSE, mountpoint,
".Trash/%d/files", (int) getuid ());
/* (2) */
mount->dirs[1] = trash_dir_new (watcher->root, watching, FALSE, mountpoint,
".Trash-%d/files", (int) getuid ());
mount->next = **mount_ptr_ptr;
**mount_ptr_ptr = mount;
*mount_ptr_ptr = &mount->next;
}
static void
trash_mount_remove (TrashMount **mount_ptr)
{
TrashMount *mount = *mount_ptr;
/* first, the dirs */
trash_dir_free (mount->dirs[0]);
trash_dir_free (mount->dirs[1]);
/* detach from list */
*mount_ptr = mount->next;
g_unix_mount_free (mount->mount_entry);
g_slice_free (TrashMount, mount);
}
static gboolean
ignore_trash_mount (GUnixMountEntry *mount)
{
GUnixMountPoint *mount_point = NULL;
const gchar *mount_options;
gboolean retval = TRUE;
if (g_unix_mount_is_system_internal (mount))
return TRUE;
mount_options = g_unix_mount_get_options (mount);
if (mount_options == NULL)
{
mount_point = g_unix_mount_point_at (g_unix_mount_get_mount_path (mount),
NULL);
if (mount_point != NULL)
mount_options = g_unix_mount_point_get_options (mount_point);
}
if (mount_options == NULL ||
strstr (mount_options, "x-gvfs-notrash") == NULL)
retval = FALSE;
g_clear_pointer (&mount_point, g_unix_mount_point_free);
return retval;
}
static void
trash_watcher_remount (TrashWatcher *watcher)
{
TrashMount **old;
GList *mounts;
GList *new;
mounts = g_unix_mounts_get (NULL);
mounts = g_list_sort (mounts, (GCompareFunc) g_unix_mount_compare);
old = &watcher->mounts;
new = mounts;
/* synchronise the two lists */
while (*old || new)
{
int result;
if (new && ignore_trash_mount (new->data))
{
g_unix_mount_free (new->data);
new = new->next;
continue;
}
if ((result = (new == NULL) - (*old == NULL)) == 0)
result = g_unix_mount_compare (new->data, (*old)->mount_entry);
if (result < 0)
{
/* new entry. add it. */
trash_mount_insert (watcher, &old, new->data);
new = new->next;
}
else if (result > 0)
{
/* old entry. remove it. */
trash_mount_remove (old);
}
else
{
/* match. no change. */
g_unix_mount_free (new->data);
old = &(*old)->next;
new = new->next;
}
}
g_list_free (mounts);
}
TrashWatcher *
trash_watcher_new (TrashRoot *root)
{
GUnixMountEntry *homedir_mount;
GFile *homedir_trashdir;
TrashWatcher *watcher;
GFile *user_datadir;
watcher = g_slice_new (TrashWatcher);
watcher->root = root;
watcher->mounts = NULL;
watcher->watching = FALSE;
watcher->mount_monitor = g_unix_mount_monitor_get ();
g_signal_connect_swapped (watcher->mount_monitor, "mounts_changed",
G_CALLBACK (trash_watcher_remount), watcher);
user_datadir = g_file_new_for_path (g_get_user_data_dir ());
homedir_trashdir = g_file_get_child (user_datadir, "Trash/files");
homedir_mount = find_mount_entry_for_file (homedir_trashdir);
watcher->homedir_type = decide_watch_type (homedir_mount, TRUE);
watcher->homedir_trashdir = trash_dir_new (watcher->root,
FALSE, TRUE,
g_get_user_data_dir (),
"Trash/files");
if (homedir_mount)
g_unix_mount_free (homedir_mount);
g_object_unref (homedir_trashdir);
g_object_unref (user_datadir);
trash_watcher_remount (watcher);
return watcher;
}
void
trash_watcher_free (TrashWatcher *watcher)
{
/* We just leak everything here, as this is not normally hit.
This used to be a g_assert_not_reached(), and that got hit when
mounting the trash backend failed due to the trash already being
mounted. */
}
void
trash_watcher_watch (TrashWatcher *watcher)
{
TrashMount *mount;
g_assert (!watcher->watching);
if (watcher->homedir_type != TRASH_WATCHER_NO_WATCH)
trash_dir_watch (watcher->homedir_trashdir);
for (mount = watcher->mounts; mount; mount = mount->next)
if (mount->type != TRASH_WATCHER_NO_WATCH)
{
trash_dir_watch (mount->dirs[0]);
trash_dir_watch (mount->dirs[1]);
}
watcher->watching = TRUE;
}
void
trash_watcher_unwatch (TrashWatcher *watcher)
{
TrashMount *mount;
g_assert (watcher->watching);
if (watcher->homedir_type != TRASH_WATCHER_NO_WATCH)
trash_dir_unwatch (watcher->homedir_trashdir);
for (mount = watcher->mounts; mount; mount = mount->next)
if (mount->type != TRASH_WATCHER_NO_WATCH)
{
trash_dir_unwatch (mount->dirs[0]);
trash_dir_unwatch (mount->dirs[1]);
}
watcher->watching = FALSE;
}
void
trash_watcher_rescan (TrashWatcher *watcher)
{
TrashMount *mount;
if (!watcher->watching || watcher->homedir_type != TRASH_WATCHER_TRUSTED)
trash_dir_rescan (watcher->homedir_trashdir);
for (mount = watcher->mounts; mount; mount = mount->next)
if (!watcher->watching || mount->type != TRASH_WATCHER_TRUSTED)
{
trash_dir_rescan (mount->dirs[0]);
trash_dir_rescan (mount->dirs[1]);
}
}