From 6ec2bb17c393c411a2182e865aa0979165dfbac5 Mon Sep 17 00:00:00 2001 From: Ryan Lortie Date: Sun, 28 Jul 2013 13:41:17 -0400 Subject: GFile: add new g_file_measure_disk_usage() API This is essentially the equivalent of 'du'. This is currently only supported on local files. gvfs will add support for the interface later. https://bugzilla.gnome.org/show_bug.cgi?id=704893 --- gio/gfile.c | 301 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ gio/gfile.h | 53 ++++++++++ gio/gioenums.h | 23 +++++ gio/giotypes.h | 43 ++++++++ gio/glocalfile.c | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 709 insertions(+) (limited to 'gio') diff --git a/gio/gfile.c b/gio/gfile.c index b64a97d35..0c9594292 100644 --- a/gio/gfile.c +++ b/gio/gfile.c @@ -316,6 +316,30 @@ static gboolean g_file_real_copy_finish (GFile GAsyncResult *res, GError **error); +static gboolean g_file_real_measure_disk_usage (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); +static void g_file_real_measure_disk_usage_async (GFile *file, + GFileMeasureFlags flags, + gint io_priority, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + GAsyncReadyCallback callback, + gpointer user_data); +static gboolean g_file_real_measure_disk_usage_finish (GFile *file, + GAsyncResult *result, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); + typedef GFileIface GFileInterface; G_DEFINE_INTERFACE (GFile, g_file, G_TYPE_OBJECT) @@ -357,6 +381,9 @@ g_file_default_init (GFileIface *iface) iface->set_attributes_from_info = g_file_real_set_attributes_from_info; iface->copy_async = g_file_real_copy_async; iface->copy_finish = g_file_real_copy_finish; + iface->measure_disk_usage = g_file_real_measure_disk_usage; + iface->measure_disk_usage_async = g_file_real_measure_disk_usage_async; + iface->measure_disk_usage_finish = g_file_real_measure_disk_usage_finish; } @@ -7336,6 +7363,280 @@ g_file_replace_contents_finish (GFile *file, return TRUE; } +gboolean +g_file_real_measure_disk_usage (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error) +{ + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Operation not supported for the current backend."); + return FALSE; +} + +typedef struct +{ + GFileMeasureFlags flags; + GFileMeasureProgressCallback progress_callback; + gpointer progress_data; +} MeasureTaskData; + +typedef struct +{ + guint64 disk_usage; + guint64 num_dirs; + guint64 num_files; +} MeasureResult; + +typedef struct +{ + GFileMeasureProgressCallback callback; + gpointer user_data; + gboolean reporting; + guint64 current_size; + guint64 num_dirs; + guint64 num_files; +} MeasureProgress; + +static gboolean +measure_disk_usage_invoke_progress (gpointer user_data) +{ + MeasureProgress *progress = user_data; + + (* progress->callback) (progress->reporting, + progress->current_size, progress->num_dirs, progress->num_files, + progress->user_data); + + return FALSE; +} + +static void +measure_disk_usage_progress (gboolean reporting, + guint64 current_size, + guint64 num_dirs, + guint64 num_files, + gpointer user_data) +{ + MeasureProgress progress; + GTask *task = user_data; + MeasureTaskData *data; + + data = g_task_get_task_data (task); + + progress.callback = data->progress_callback; + progress.user_data = data->progress_data; + progress.reporting = reporting; + progress.current_size = current_size; + progress.num_dirs = num_dirs; + progress.num_files = num_files; + + g_main_context_invoke_full (g_task_get_context (task), + g_task_get_priority (task), + measure_disk_usage_invoke_progress, + g_memdup (&progress, sizeof progress), + g_free); +} + +static void +measure_disk_usage_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + MeasureTaskData *data = task_data; + GError *error = NULL; + MeasureResult result; + + if (g_file_measure_disk_usage (source_object, data->flags, cancellable, + measure_disk_usage_progress, task, + &result.disk_usage, &result.num_dirs, &result.num_files, + &error)) + g_task_return_pointer (task, g_memdup (&result, sizeof result), g_free); + else + g_task_return_error (task, error); +} + +static void +g_file_real_measure_disk_usage_async (GFile *file, + GFileMeasureFlags flags, + gint io_priority, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MeasureTaskData data; + GTask *task; + + data.flags = flags; + data.progress_callback = progress_callback; + data.progress_data = progress_data; + + task = g_task_new (file, cancellable, callback, user_data); + g_task_set_task_data (task, g_memdup (&data, sizeof data), g_free); + g_task_set_priority (task, io_priority); + + g_task_run_in_thread (task, measure_disk_usage_thread); + g_object_unref (task); +} + +static gboolean +g_file_real_measure_disk_usage_finish (GFile *file, + GAsyncResult *result, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error) +{ + guint64 *reported_usage; + + g_return_val_if_fail (g_task_is_valid (result, file), FALSE); + + reported_usage = g_task_propagate_pointer (G_TASK (result), error); + + if (reported_usage == NULL) + return FALSE; + + if (disk_usage) + *disk_usage = *reported_usage; + + g_free (reported_usage); + + return TRUE; +} + +/** + * g_file_measure_disk_usage: + * @file: a #GFile + * @flags: #GFileMeasureFlags + * @cancellable: (allow-none): optional #GCancellable + * @progress_callback: (allow-none): a #GFileMeasureProgressCallback + * @progress_data: user_data for @progress_callback + * @disk_usage: (allow-none) (out): the number of bytes of disk space used + * @num_dirs: (allow-none) (out): the number of directories encountered + * @num_files: (allow-none) (out): the number of non-directories encountered + * @error: (allow-none): %NULL, or a pointer to a %NULL #GError pointer + * + * Recursively measures the disk usage of @file. + * + * This is essentially an analog of the 'du' command, + * but it also reports the number of directories and non-directory files + * encountered (including things like symbolic links). + * + * By default, errors are only reported against the toplevel file + * itself. Errors found while recursing are silently ignored, unless + * %G_FILE_DISK_USAGE_REPORT_ALL_ERRORS is given in @flags. + * + * The returned size, @disk_usage, is in bytes and should be formatted + * with g_format_size() in order to get something reasonable for showing + * in a user interface. + * + * @progress_callback and @progress_data can be given to request + * periodic progress updates while scanning. See the documentation for + * #GFileMeasureProgressCallback for information about when and how the + * callback will be invoked. + * + * Returns: %TRUE if successful, with the out parameters set. + * %FALSE otherwise, with @error set. + * + * Since: 2.38 + **/ +gboolean +g_file_measure_disk_usage (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error) +{ + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return G_FILE_GET_IFACE (file)->measure_disk_usage (file, flags, cancellable, + progress_callback, progress_data, + disk_usage, num_dirs, num_files, + error); +} + +/** + * g_file_measure_disk_usage_async: + * @file: a #GFile + * @flags: #GFileMeasureFlags + * @io_priority: the I/O priority + * of the request + * @cancellable: (allow-none): optional #GCancellable + * @progress_callback: (allow-none): a #GFileMeasureProgressCallback + * @progress_data: user_data for @progress_callback + * @callback: (allow-none): a #GAsyncReadyCallback to call when complete + * @user_data: the data to pass to callback function + * + * Recursively measures the disk usage of @file. + * + * This is the asynchronous version of g_file_measure_disk_usage(). See + * there for more information. + * + * Since: 2.38 + **/ +void +g_file_measure_disk_usage_async (GFile *file, + GFileMeasureFlags flags, + gint io_priority, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (G_IS_FILE (file)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + return G_FILE_GET_IFACE (file)->measure_disk_usage_async (file, flags, io_priority, cancellable, + progress_callback, progress_data, + callback, user_data); +} + +/** + * g_file_measure_disk_usage_finish: + * @file: a #GFile + * @result: the #GAsyncResult passed to your #GAsyncReadyCallback + * @disk_usage: (allow-none) (out): the number of bytes of disk space used + * @num_dirs: (allow-none) (out): the number of directories encountered + * @num_files: (allow-none) (out): the number of non-directories encountered + * @error: (allow-none): %NULL, or a pointer to a %NULL #GError pointer + * + * Collects the results from an earlier call to + * g_file_measure_disk_usage_async(). See g_file_measure_disk_usage() for + * more information. + * + * Returns: %TRUE if successful, with the out parameters set. + * %FALSE otherwise, with @error set. + * + * Since: 2.38 + **/ +gboolean +g_file_measure_disk_usage_finish (GFile *file, + GAsyncResult *result, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error) +{ + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return G_FILE_GET_IFACE (file)->measure_disk_usage_finish (file, result, disk_usage, num_dirs, num_files, error); +} + /** * g_file_start_mountable: * @file: input #GFile diff --git a/gio/gfile.h b/gio/gfile.h index 0cf6ee28f..394c643fb 100644 --- a/gio/gfile.h +++ b/gio/gfile.h @@ -561,6 +561,30 @@ struct _GFileIface gboolean (* poll_mountable_finish) (GFile *file, GAsyncResult *result, GError **error); + + gboolean (* measure_disk_usage) (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); + void (* measure_disk_usage_async) (GFile *file, + GFileMeasureFlags flags, + gint io_priority, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + GAsyncReadyCallback callback, + gpointer user_data); + gboolean (* measure_disk_usage_finish) (GFile *file, + GAsyncResult *result, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); }; GLIB_AVAILABLE_IN_ALL @@ -1085,6 +1109,35 @@ GFileMonitor* g_file_monitor (GFile GCancellable *cancellable, GError **error); +GLIB_AVAILABLE_IN_2_38 +gboolean g_file_measure_disk_usage (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); + +GLIB_AVAILABLE_IN_2_38 +void g_file_measure_disk_usage_async (GFile *file, + GFileMeasureFlags flags, + gint io_priority, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + GAsyncReadyCallback callback, + gpointer user_data); + +GLIB_AVAILABLE_IN_2_38 +gboolean g_file_measure_disk_usage_finish (GFile *file, + GAsyncResult *result, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error); + GLIB_AVAILABLE_IN_ALL void g_file_start_mountable (GFile *file, GDriveStartFlags flags, diff --git a/gio/gioenums.h b/gio/gioenums.h index 976722639..4ce0855cb 100644 --- a/gio/gioenums.h +++ b/gio/gioenums.h @@ -211,6 +211,29 @@ typedef enum { G_FILE_CREATE_REPLACE_DESTINATION = (1 << 1) } GFileCreateFlags; +/** + * GFileMeasureFlags: + * @G_FILE_MEASURE_NONE: No flags set. + * @G_FILE_MEASURE_REPORT_ANY_ERROR: Report any error encountered + * while traversing the directory tree. Normally errors are only + * reported for the toplevel file. + * @G_FILE_MEASURE_APPARENT_SIZE: Tally usage based on apparent file + * sizes. Normally, the block-size is used, if available, as this is a + * more accurate representation of disk space used. + * Compare with 'du --apparent-size'. + * @G_FILE_MEASURE_NO_XDEV: Do not cross mount point boundaries. + * Compare with 'du -x'. + * + * Flags that can be used with g_file_measure_disk_usage(). + * + * Since: 2.38 + **/ +typedef enum { + G_FILE_MEASURE_NONE = 0, + G_FILE_MEASURE_REPORT_ANY_ERROR = (1 << 1), + G_FILE_MEASURE_APPARENT_SIZE = (1 << 2), + G_FILE_MEASURE_NO_XDEV = (1 << 3) +} GFileMeasureFlags; /** * GMountMountFlags: diff --git a/gio/giotypes.h b/gio/giotypes.h index adcbdae67..fdf720235 100644 --- a/gio/giotypes.h +++ b/gio/giotypes.h @@ -295,6 +295,49 @@ typedef gboolean (* GFileReadMoreCallback) (const char *file_contents, goffset file_size, gpointer callback_data); +/** + * GFileMeasureProgressCallback: + * @reporting: %TRUE if more reports will come + * @current_size: the current cumulative size measurement + * @num_dirs: the number of directories visited so far + * @num_files: the number of non-directory files encountered + * @user_data: the data passed to the original request for this callback + * + * This callback type is used by g_file_measure_disk_usage() to make + * periodic progress reports when measuring the amount of disk spaced + * used by a directory. + * + * These calls are made on a best-effort basis and not all types of + * #GFile will support them. At the minimum, however, one call will + * always be made immediately. + * + * In the case that there is no support, @reporting will be set to + * %FALSE (and the other values undefined) and no further calls will be + * made. Otherwise, the @reporting will be %TRUE and the other values + * all-zeros during the first (immediate) call. In this way, you can + * know which type of progress UI to show without a delay. + * + * For g_file_measure_disk_usage() the callback is made directly. For + * g_file_measure_disk_usage_async() the callback is made via the + * default main context of the calling thread (ie: the same way that the + * final async result would be reported). + * + * @current_size is in the same units as requested by the operation (see + * %G_FILE_DISK_USAGE_APPARENT_SIZE). + * + * The frequency of the updates is implementation defined, but is + * ideally about once every 200ms. + * + * The last progress callback may or may not be equal to the final + * result. Always check the async result to get the final value. + * + * Since: 2.38 + **/ +typedef void (* GFileMeasureProgressCallback) (gboolean reporting, + guint64 current_size, + guint64 num_dirs, + guint64 num_files, + gpointer user_data); /** * GIOSchedulerJobFunc: diff --git a/gio/glocalfile.c b/gio/glocalfile.c index 67e831311..ea590185a 100644 --- a/gio/glocalfile.c +++ b/gio/glocalfile.c @@ -27,6 +27,7 @@ #include #include #include +#include #ifdef HAVE_UNISTD_H #include #endif @@ -2514,6 +2515,293 @@ g_local_file_monitor_file (GFile *file, return _g_local_file_monitor_new (local_file->filename, flags, is_remote (local_file->filename), error); } + +/* Here is the GLocalFile implementation of g_file_measure_disk_usage(). + * + * If available, we use fopenat() in preference to filenames for + * efficiency and safety reasons. We know that fopenat() is available + * based on if AT_FDCWD is defined. POSIX guarantees that this will be + * defined as a macro. + * + * We use a linked list of stack-allocated GSList nodes in order to be + * able to reconstruct the filename for error messages. We actually + * pass the filename to operate on through the top node of the list. + * + * In case we're using openat(), this top filename will be a basename + * which should be opened in the directory which has also had its fd + * passed along. If we're not using openat() then it will be a full + * absolute filename. + */ + +static gboolean +g_local_file_measure_size_error (GFileMeasureFlags flags, + gint saved_errno, + GSList *name, + GError **error) +{ + /* Only report an error if we were at the toplevel or if the caller + * requested reporting of all errors. + */ + if ((name->next == NULL) || (flags & G_FILE_MEASURE_REPORT_ANY_ERROR)) + { + GString *filename; + GSList *node; + + /* Skip some work if there is no error return */ + if (!error) + return FALSE; + +#ifdef AT_FDCWD + /* If using openat() we need to rebuild the filename for the message */ + filename = g_string_new (name->data); + for (node = name->next; node; node = node->next) + { + g_string_prepend_c (filename, G_DIR_SEPARATOR); + g_string_prepend (filename, node->data); + } + + g_string_prepend (filename, "file://"); +#else + /* Otherwise, we already have it, so just use it. */ + node = name; + filename = g_string_new ("file://"); + g_string_append (filename, node->data); +#endif + + g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), + _("Could not determine the disk usage of %s: %s"), + filename->str, g_strerror (saved_errno)); + + g_string_free (filename, TRUE); + + return FALSE; + } + + else + /* We're not reporting this error... */ + return TRUE; +} + +typedef struct +{ + GFileMeasureFlags flags; + dev_t contained_on; + GCancellable *cancellable; + + GFileMeasureProgressCallback progress_callback; + gpointer progress_data; + + guint64 disk_usage; + guint64 num_dirs; + guint64 num_files; + + guint64 last_progress_report; +} MeasureState; + +static gboolean +g_local_file_measure_size_of_contents (gint fd, + GSList *dir_name, + MeasureState *state, + GError **error); + +static gboolean +g_local_file_measure_size_of_file (gint parent_fd, + GSList *name, + MeasureState *state, + GError **error) +{ + struct stat buf; + + if (g_cancellable_set_error_if_cancelled (state->cancellable, error)) + return FALSE; + +#if defined (AT_FDCWD) + if (fstatat (parent_fd, name->data, &buf, AT_SYMLINK_NOFOLLOW) != 0) +#elif defined (HAVE_LSTAT) + if (lstat (name->data, &buf) != 0) +#else + if (stat (name->data, &buf) != 0) +#endif + return g_local_file_measure_size_error (state->flags, errno, name, error); + + if (name->next) + { + /* If not at the toplevel, check for a device boundary. */ + + if (state->flags & G_FILE_MEASURE_NO_XDEV) + if (state->contained_on != buf.st_dev) + return TRUE; + } + else + { + /* If, however, this is the toplevel, set the device number so + * that recursive invocations can compare against it. + */ + state->contained_on = buf.st_dev; + } + +#if defined (HAVE_STRUCT_STAT_ST_BLOCKS) + if (~state->flags & G_FILE_MEASURE_APPARENT_SIZE) + state->disk_usage += buf.st_blocks * G_GUINT64_CONSTANT (512); + else +#endif + state->disk_usage += buf.st_size; + + if (S_ISDIR (buf.st_mode)) + state->num_dirs++; + else + state->num_files++; + + if (state->progress_callback) + { + /* We could attempt to do some cleverness here in order to avoid + * calling clock_gettime() so much, but we're doing stats and opens + * all over the place already... + */ + if (state->last_progress_report) + { + guint64 now; + + now = g_get_monotonic_time (); + + if (state->last_progress_report + 200 * G_TIME_SPAN_MILLISECOND < now) + { + (* state->progress_callback) (TRUE, + state->disk_usage, state->num_dirs, state->num_files, + state->progress_data); + state->last_progress_report = now; + } + } + else + { + /* We must do an initial report to inform that more reports + * will be coming. + */ + (* state->progress_callback) (TRUE, 0, 0, 0, state->progress_data); + state->last_progress_report = g_get_monotonic_time (); + } + } + + if (S_ISDIR (buf.st_mode)) + { + int dir_fd = -1; + + if (g_cancellable_set_error_if_cancelled (state->cancellable, error)) + return FALSE; + +#ifdef AT_FDCWD + dir_fd = openat (parent_fd, name->data, O_RDONLY | O_DIRECTORY); + if (dir_fd < 0) + return g_local_file_measure_size_error (state->flags, errno, name, error); +#endif + + if (!g_local_file_measure_size_of_contents (dir_fd, name, state, error)) + return FALSE; + } + + return TRUE; +} + +static gboolean +g_local_file_measure_size_of_contents (gint fd, + GSList *dir_name, + MeasureState *state, + GError **error) +{ + gboolean success = TRUE; + struct dirent *entry; + DIR *dirp; + +#ifdef AT_FDCWD + dirp = fdopendir (fd); +#else + dirp = opendir (dir_name->data); +#endif + + if (dirp == NULL) + { + gint saved_errno = errno; + +#ifdef AT_FDCWD + close (fd); +#endif + + return g_local_file_measure_size_error (state->flags, saved_errno, dir_name, error); + } + + while (success && (entry = readdir (dirp))) + { + gchar *name = entry->d_name; + GSList node; + + node.next = dir_name; +#ifdef AT_FDCWD + node.data = name; +#else + node.data = g_build_filename (dir_name->data, name, NULL); +#endif + + /* skip '.' and '..' */ + if (name[0] == '.' && + (name[1] == '\0' || + (name[1] == '.' && name[2] == '\0'))) + continue; + + success = g_local_file_measure_size_of_file (fd, &node, state, error); + +#ifndef AT_FDCWD + g_free (node.data); +#endif + } + + closedir (dirp); + + return success; +} + +static gboolean +g_local_file_measure_disk_usage (GFile *file, + GFileMeasureFlags flags, + GCancellable *cancellable, + GFileMeasureProgressCallback progress_callback, + gpointer progress_data, + guint64 *disk_usage, + guint64 *num_dirs, + guint64 *num_files, + GError **error) +{ + GLocalFile *local_file = G_LOCAL_FILE (file); + MeasureState state = { 0, }; + gint root_fd = -1; + GSList node; + + state.flags = flags; + state.cancellable = cancellable; + state.progress_callback = progress_callback; + state.progress_data = progress_data; + +#ifdef AT_FDCWD + root_fd = AT_FDCWD; +#endif + + node.data = local_file->filename; + node.next = NULL; + + if (!g_local_file_measure_size_of_file (root_fd, &node, &state, error)) + return FALSE; + + if (disk_usage) + *disk_usage = state.disk_usage; + + if (num_dirs) + *num_dirs = state.num_dirs; + + if (num_files) + *num_files = state.num_files; + + return TRUE; +} + static void g_local_file_file_iface_init (GFileIface *iface) { @@ -2556,6 +2844,7 @@ g_local_file_file_iface_init (GFileIface *iface) iface->move = g_local_file_move; iface->monitor_dir = g_local_file_monitor_dir; iface->monitor_file = g_local_file_monitor_file; + iface->measure_disk_usage = g_local_file_measure_disk_usage; iface->supports_thread_contexts = TRUE; } -- cgit v1.2.3