🏡 index : quine-global-labs/bazaar.git

/* io.c
 *
 * Copyright 2025 Adam Masciola
 *
 * 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 3 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "io.h"
#include "bz-size-result.h"
#include "env.h"

static DexFuture *
reap_file_fiber (GFile *file);
static DexFuture *
reap_path_fiber (char *path);
static DexFuture *
reap_app_dir_fiber (char *path);

static DexFuture *
file_measure_disk_usage_dex (GFile *file);
static DexFuture *
get_user_sizes_fiber (char *app_id);
static DexFuture *
get_all_user_data_ids_fiber (void);

char *
bz_dup_user_data_path (const char *app_id)
{
  g_return_val_if_fail (app_id != NULL, NULL);
  return g_build_filename (g_get_home_dir (), ".var", "app", app_id, NULL);
}

char *
bz_dup_user_cache_path (const char *app_id)
{
  g_return_val_if_fail (app_id != NULL, NULL);
  return g_build_filename (g_get_home_dir (), ".var", "app", app_id, "cache", NULL);
}

DexScheduler *
bz_get_io_scheduler (void)
{
  static DexScheduler *scheduler = NULL;

  if (g_once_init_enter_pointer (&scheduler))
    g_once_init_leave_pointer (&scheduler, dex_thread_pool_scheduler_new ());

  return scheduler;
}

DexLimiter *
bz_get_io_limiter (void)
{
  static DexLimiter *limiter = NULL;

  if (g_once_init_enter_pointer (&limiter))
    {
      guint concurrent_io = 0;

      /* Ensure we don't overload the system with work; aim for # of logical
         processors divided by 2

        See:
          https://gitlab.gnome.org/World/bazaar/-/work_items/497
          https://docs.gtk.org/glib/func.get_num_processors.html */
      concurrent_io = MIN (32, MAX (1, g_get_num_processors () / 2));

      g_once_init_leave_pointer (&limiter, dex_limiter_new (concurrent_io));
    }

  return limiter;
}

void
bz_reap_file (GFile *file)
{
  g_autoptr (GError) local_error         = NULL;
  g_autofree gchar *uri                  = NULL;
  g_autoptr (GFileEnumerator) enumerator = NULL;
  gboolean result                        = FALSE;

  g_return_if_fail (G_IS_FILE (file));

  uri = g_file_get_uri (file);
  if (uri == NULL)
    uri = g_file_get_path (file);

  enumerator = g_file_enumerate_children (
      file,
      G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK
      "," G_FILE_ATTRIBUTE_STANDARD_NAME
      "," G_FILE_ATTRIBUTE_STANDARD_TYPE
      "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
      G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
      NULL,
      &local_error);
  if (enumerator == NULL)
    {
      if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        g_warning ("failed to reap cache directory '%s': %s", uri, local_error->message);
      g_clear_pointer (&local_error, g_error_free);
      return;
    }

  for (;;)
    {
      g_autoptr (GFileInfo) info = NULL;
      g_autoptr (GFile) child    = NULL;
      GFileType file_type        = G_FILE_TYPE_UNKNOWN;

      info = g_file_enumerator_next_file (enumerator, NULL, &local_error);
      if (info == NULL)
        {
          if (local_error != NULL)
            g_warning ("failed to enumerate cache directory '%s': %s", uri, local_error->message);
          g_clear_pointer (&local_error, g_error_free);
          break;
        }

      child     = g_file_enumerator_get_child (enumerator, info);
      file_type = g_file_info_get_file_type (info);

      if (!g_file_info_get_is_symlink (info) && file_type == G_FILE_TYPE_DIRECTORY)
        bz_reap_file (child);
      else
        {
          result = g_file_delete (child, NULL, &local_error);
          if (!result)
            {
              g_warning ("failed to reap cache directory '%s': %s", uri, local_error->message);
              g_clear_pointer (&local_error, g_error_free);
            }
        }
    }

  result = g_file_enumerator_close (enumerator, NULL, &local_error);
  if (!result)
    {
      g_warning ("failed to reap cache directory '%s': %s", uri, local_error->message);
      g_clear_pointer (&local_error, g_error_free);
    }

  result = g_file_delete (file, NULL, &local_error);
  if (!result)
    {
      g_warning ("failed to reap cache directory '%s': %s", uri, local_error->message);
      g_clear_pointer (&local_error, g_error_free);
    }
}

void
bz_reap_path (const char *path)
{
  g_autoptr (GFile) file = NULL;

  g_return_if_fail (path != NULL);

  file = g_file_new_for_path (path);
  bz_reap_file (file);
}

static void
trash_file_callback (GObject      *object,
                     GAsyncResult *result,
                     gpointer      user_data)
{
  g_autoptr (DexPromise) promise = user_data;
  g_autoptr (GError) error       = NULL;

  if (g_file_trash_finish (G_FILE (object), result, &error))
    dex_promise_resolve_boolean (promise, TRUE);
  else
    dex_promise_reject (promise, g_steal_pointer (&error));
}

static DexFuture *
trash_file_dex (GFile *file)
{
  DexPromise *promise = dex_promise_new_cancellable ();

  g_file_trash_async (file,
                      G_PRIORITY_DEFAULT,
                      dex_promise_get_cancellable (promise),
                      trash_file_callback,
                      dex_ref (promise));

  return DEX_FUTURE (promise);
}

static DexFuture *
reap_app_dir_fiber (char *path)
{
  g_autoptr (GFile) file             = NULL;
  g_autoptr (DexFuture) trash_future = NULL;
  g_autoptr (GError) error           = NULL;
  gboolean result                    = FALSE;

  file         = g_file_new_for_path (path);
  trash_future = trash_file_dex (file);
  result       = dex_await_boolean (dex_ref (trash_future), &error);

  if (!result)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        return dex_future_new_true ();
      g_warning ("failed to trash directory '%s': %s", path, error->message);
      return dex_future_new_for_error (g_steal_pointer (&error));
    }

  return dex_future_new_true ();
}

static DexFuture *
reap_user_data_fiber (char *app_id)
{
  g_autofree char *path = bz_dup_user_data_path (app_id);
  return reap_app_dir_fiber (path);
}

static DexFuture *
reap_user_cache_fiber (char *app_id)
{
  g_autofree char *path = bz_dup_user_cache_path (app_id);

  bz_reap_path (path);
  return dex_future_new_true ();
}

DexFuture *
bz_reap_file_dex (GFile *file)
{
  dex_return_error_if_fail (G_IS_FILE (file));
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) reap_file_fiber,
      g_object_ref (file), g_object_unref);
}

DexFuture *
bz_reap_path_dex (const char *path)
{
  dex_return_error_if_fail (path != NULL);
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) reap_path_fiber,
      g_strdup (path), g_free);
}

DexFuture *
bz_user_data_exists (const char *app_id)
{
  g_autofree char *user_data_path = NULL;

  g_return_val_if_fail (app_id != NULL, NULL);

  user_data_path = bz_dup_user_data_path (app_id);
  return dex_file_query_exists (g_file_new_for_path (user_data_path));
}

DexFuture *
bz_reap_user_data_dex (const char *app_id)
{
  dex_return_error_if_fail (app_id != NULL);
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) reap_user_data_fiber,
      g_strdup (app_id), g_free);
}

DexFuture *
bz_reap_user_cache_dex (const char *app_id)
{
  dex_return_error_if_fail (app_id != NULL);
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) reap_user_cache_fiber,
      g_strdup (app_id), g_free);
}

DexFuture *
bz_get_user_sizes_dex (const char *app_id)
{
  dex_return_error_if_fail (app_id != NULL);
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) get_user_sizes_fiber,
      g_strdup (app_id), g_free);
}

DexFuture *
bz_get_user_data_ids_dex (void)
{
  return dex_limiter_run (
      bz_get_io_limiter (),
      bz_get_io_scheduler (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) get_all_user_data_ids_fiber,
      NULL, NULL);
}

static DexFuture *
get_user_sizes_fiber (char *app_id)
{
  g_autofree char *user_data_path  = NULL;
  g_autofree char *user_cache_path = NULL;
  g_autoptr (GFile) data_file      = NULL;
  g_autoptr (GFile) cache_file     = NULL;
  g_autoptr (GPtrArray) futures    = NULL;
  g_autoptr (GError) error         = NULL;
  guint64 user_data_size           = 0;
  guint64 cache_size               = 0;

  user_data_path  = bz_dup_user_data_path (app_id);
  user_cache_path = bz_dup_user_cache_path (app_id);
  data_file       = g_file_new_for_path (user_data_path);
  cache_file      = g_file_new_for_path (user_cache_path);

  futures = g_ptr_array_new_with_free_func (dex_unref);
  g_ptr_array_add (futures, file_measure_disk_usage_dex (data_file));
  g_ptr_array_add (futures, file_measure_disk_usage_dex (cache_file));

  dex_await (dex_future_allv (
                 (DexFuture *const *) futures->pdata,
                 futures->len),
             NULL);

  user_data_size = dex_await_uint64 (dex_ref (g_ptr_array_index (futures, 0)), &error);
  if (error != NULL)
    {
      g_clear_error (&error);
      user_data_size = 0;
    }

  cache_size = dex_await_uint64 (dex_ref (g_ptr_array_index (futures, 1)), &error);
  if (error != NULL)
    {
      g_clear_error (&error);
      cache_size = 0;
    }

  return dex_future_new_take_object (
      g_object_new (BZ_TYPE_SIZE_RESULT,
                    "user-data-size", user_data_size,
                    "cache-size", cache_size,
                    NULL));
}

static void
measure_disk_usage_callback (GObject      *object,
                             GAsyncResult *result,
                             gpointer      user_data)
{
  g_autoptr (DexPromise) promise = user_data;
  g_autoptr (GError) error       = NULL;
  guint64 disk_usage             = 0;

  if (g_file_measure_disk_usage_finish (G_FILE (object), result, &disk_usage, NULL, NULL, &error))
    dex_promise_resolve_uint64 (promise, disk_usage);
  else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
    dex_promise_resolve_uint64 (promise, 0);
  else
    dex_promise_reject (promise, g_steal_pointer (&error));
}

static DexFuture *
file_measure_disk_usage_dex (GFile *file)
{
  DexPromise *promise = NULL;
  promise             = dex_promise_new_cancellable ();

  g_file_measure_disk_usage_async (
      file,
      G_FILE_MEASURE_NONE,
      G_PRIORITY_DEFAULT,
      dex_promise_get_cancellable (promise),
      NULL, NULL,
      measure_disk_usage_callback,
      dex_ref (promise));

  return DEX_FUTURE (promise);
}

static DexFuture *
get_all_user_data_ids_fiber (void)
{
  g_autofree char *var_app_path           = NULL;
  g_autoptr (GFile) var_app_dir           = NULL;
  g_autoptr (DexFuture) enumerator_future = NULL;
  g_autoptr (GFileEnumerator) enumerator  = NULL;
  g_autoptr (GError) error                = NULL;
  GHashTable *ids                         = NULL;

  var_app_path = g_build_filename (g_get_home_dir (), ".var", "app", NULL);
  var_app_dir  = g_file_new_for_path (var_app_path);

  ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  enumerator_future = dex_file_enumerate_children (
      var_app_dir,
      G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
      G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
      G_PRIORITY_DEFAULT);

  enumerator = dex_await_object (dex_ref (enumerator_future), &error);
  if (enumerator == NULL)
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
        return dex_future_new_take_boxed (G_TYPE_HASH_TABLE, ids);
      g_hash_table_unref (ids);
      return dex_future_new_for_error (g_steal_pointer (&error));
    }

  for (;;)
    {
      g_autoptr (DexFuture) next_future = NULL;
      g_autolist (GFileInfo) infos      = NULL;

      next_future = dex_file_enumerator_next_files (enumerator, 10, G_PRIORITY_DEFAULT);
      infos       = dex_await_boxed (dex_ref (next_future), &error);

      if (infos == NULL)
        {
          if (error != NULL)
            {
              g_hash_table_unref (ids);
              return dex_future_new_for_error (g_steal_pointer (&error));
            }
          break;
        }

      for (GList *l = infos; l != NULL; l = l->next)
        {
          GFileInfo *info      = l->data;
          GFileType  file_type = g_file_info_get_file_type (info);

          if (file_type == G_FILE_TYPE_DIRECTORY)
            {
              const char *app_id = g_file_info_get_name (info);
              g_hash_table_insert (ids, g_strdup (app_id), NULL);
            }
        }
    }

  return dex_future_new_take_boxed (G_TYPE_HASH_TABLE, ids);
}

char *
bz_dup_root_cache_dir (void)
{
  GApplication *application = NULL;
  const char   *user_cache  = NULL;
  const char   *id          = NULL;

  user_cache = g_get_user_cache_dir ();

  application = g_application_get_default ();
  if (application != NULL)
    id = g_application_get_application_id (application);
  if (id == NULL)
    id = "io.github.kolunmi.Bazaar";

  return g_build_filename (user_cache, id, NULL);
}

char *
bz_dup_cache_dir (const char *submodule)
{
  g_autofree char *root_cache_dir = NULL;

  g_return_val_if_fail (submodule != NULL, NULL);

  root_cache_dir = bz_dup_root_cache_dir ();
  return g_build_filename (root_cache_dir, submodule, NULL);
}

static DexFuture *
reap_file_fiber (GFile *file)
{
  bz_reap_file (file);
  return dex_future_new_true ();
}

static DexFuture *
reap_path_fiber (char *path)
{
  bz_reap_path (path);
  return dex_future_new_true ();
}