/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*-
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: Michael Terry
 */

using GLib;

[CCode (cheader_filename = "unistd.h")]
extern long gethostid();

namespace DejaDup {

public const string WINDOW_WIDTH_KEY = "window-width";
public const string WINDOW_HEIGHT_KEY = "window-height";
public const string WINDOW_MAXIMIZED_KEY = "window-maximized";
public const string WINDOW_FULLSCREENED_KEY = "window-fullscreened";
public const string INCLUDE_LIST_KEY = "include-list";
public const string EXCLUDE_LIST_KEY = "exclude-list";
public const string BACKEND_KEY = "backend";
public const string LAST_RUN_KEY = "last-run"; // started a backup
public const string LAST_BACKUP_KEY = "last-backup";
public const string LAST_RESTORE_KEY = "last-restore";
public const string PROMPT_CHECK_KEY = "prompt-check";
public const string NAG_CHECK_KEY = "nag-check";
public const string PERIODIC_KEY = "periodic";
public const string PERIODIC_PERIOD_KEY = "periodic-period";
public const string PERIODIC_TIMESTAMP_KEY = "periodic-timestamp";
public const string DELETE_AFTER_KEY = "delete-after";
public const string FULL_BACKUP_PERIOD_KEY = "full-backup-period";
public const string ALLOW_METERED_KEY = "allow-metered";
public const string ALLOW_POWER_SAVER_KEY = "allow-power-saver";
public const string _TOOL_KEY = "tool"; // deprecated
public const string TOOL_WHEN_NEW_KEY = "tool-when-new";
public const string CUSTOM_TOOL_SETUP_KEY = "custom-tool-setup";
public const string CUSTOM_TOOL_TEARDOWN_KEY = "custom-tool-teardown";
public const string CUSTOM_TOOL_WRAPPER_KEY = "custom-tool-wrapper";

public errordomain BackupError {
  BAD_CONFIG,
  ALREADY_RUNNING
}

public bool in_unit_testing_mode()
{
  var testing_str = Environment.get_variable("DEJA_DUP_TESTING");
  return (testing_str != null && int.parse(testing_str) == 50);
}

public bool in_testing_mode()
{
  var testing_str = Environment.get_variable("DEJA_DUP_TESTING");
  return (testing_str != null && int.parse(testing_str) > 0);
}

public bool in_demo_mode()
{
  return Config.PROFILE == "Screenshot";
}

public DateTime now()
{
  if (in_unit_testing_mode())
    return new DateTime(new TimeZone.local(), 2023, 8, 23, 15, 15, 15);
  return new DateTime.now_local();
}

public string current_time_as_iso8601()
{
  return now().format_iso8601();
}

public void update_last_run_timestamp(string key)
{
  var settings = get_settings();
  settings.set_string(key, current_time_as_iso8601());
}

// We manually reference this method, because Vala does not give us (as of
// 0.36 anyway...) a non-deprecated version that can still specify a base.
// So we use this one to avoid deprecation warnings during build.
[CCode (cname = "g_ascii_strtoull")]
public extern uint64 strtoull(string nptr, out char* endptr, uint _base);

public bool parse_version(string version_string, out int major, out int minor,
                          out int micro)
{
  major = 0;
  minor = 0;
  micro = 0;

  var ver_tokens = version_string.split(".");
  if (ver_tokens == null || ver_tokens[0] == null)
    return false;

  major = int.parse(ver_tokens[0]);
  // Don't error out if no minor or micro.
  if (ver_tokens[1] != null) {
    minor = int.parse(ver_tokens[1]);
    if (ver_tokens[2] != null)
      micro = int.parse(ver_tokens[2]);
  }

  return true;
}

public bool equals_version(int major, int minor, int micro,
                           int req_major, int req_minor, int req_micro)
{
  return major == req_major && minor == req_minor && micro == req_micro;
}

public bool meets_version(int major, int minor, int micro,
                          int req_major, int req_minor, int req_micro)
{
  return (major > req_major) ||
         (major == req_major && minor > req_minor) ||
         (major == req_major && minor == req_minor && micro >= req_micro);
}

public string[] nice_prefix(string[] command)
{
  int major, minor, micro;
  var utsname = Posix.utsname();
  parse_version(utsname.release, out major, out minor, out micro);

  string[] cmd = {};

  // Check for ionice to be a good disk citizen
  if (Environment.find_program_in_path("ionice") != null) {
    // In Linux 2.6.25 and up, even normal users can request idle class
    if (utsname.sysname == "Linux" && meets_version(major, minor, micro, 2, 6, 25))
      cmd = {"ionice", "-t", "-c3"}; // idle class
    else
      cmd = {"ionice", "-t", "-c2", "-n7"}; // lowest priority in best-effort class
  }

  // chrt's idle class is more-idle than nice, so prefer it
  if (utsname.sysname == "Linux" &&
      meets_version(major, minor, micro, 2, 6, 23) &&
      Environment.find_program_in_path("chrt") != null) {
    cmd += "chrt";
    cmd += "--idle";
    cmd += "0";
  }
  else if (Environment.find_program_in_path("nice") != null) {
    cmd += "nice";
    cmd += "-n19";
  }

  foreach (var fragment in command)
    cmd += fragment;

  return cmd;
}

public void run_command(string[] args = {})
{
  var argv = nice_prefix(args);

  try {
    Process.spawn_async(null, argv, null,
                        SpawnFlags.SEARCH_PATH/* |
                        SpawnFlags.STDOUT_TO_DEV_NULL |
                        SpawnFlags.STDERR_TO_DEV_NULL*/,
                        null, null);
  } catch (Error e) {
    warning("%s\n", e.message);
  }
}

public string get_monitor_exec()
{
  var monitor_exec = Environment.get_variable("DEJA_DUP_MONITOR_EXEC");
  if (monitor_exec != null && monitor_exec.length > 0)
    return monitor_exec;
  return Path.build_filename(Config.PKG_LIBEXEC_DIR, "deja-dup-monitor");
}

public string get_application_path()
{
  return "/org/gnome/DejaDup" + Config.PROFILE;
}

uint32 machine_id = 0;
uint32 get_machine_id()
{
  if (machine_id > 0)
    return machine_id;

  // First try /etc/machine-id, then /var/lib/dbus/machine-id, then hostid

  if (in_testing_mode())
    return 1;

  string machine_string;
  try {
    FileUtils.get_contents("/etc/machine-id", out machine_string);
  }
  catch (Error e) {}

  if (machine_string == null) {
    try {
      FileUtils.get_contents("/var/lib/dbus/machine-id", out machine_string);
    }
    catch (Error e) {}
  }

  if (machine_string != null)
    machine_id = (uint32)strtoull(machine_string, null, 16);

  if (machine_id == 0)
    machine_id = (uint32)gethostid();

  return machine_id;
}

// Takes a time and either moves it forward or backward to the middle of the
// night. "Midnight" is just being cute, we actually move it to somewhere
// in the 2-4 AM range (local time).
//
// We do this because:
// (A) In cases like cloud services or shared servers, it will help to avoid
//     all users hitting the server at the same time.  Hence the local time
//     and randomization.
//     See https://bugs.launchpad.net/deja-dup/+bug/1154920 for example.
// (B) Randomizing around 2-4 AM is probably a decent guess as for when to
//     back up, if the user leaves the machine on and in the absence of more
//     advanced scheduling support.
//     Scheduling ticket: https://gitlab.gnome.org/World/deja-dup/-/issues/111
// (C) We randomize using machine id as a seed to make our predictions
//     consistent between calls to this function and runs of deja-dup.
public DateTime fuzz_to_midnight(DateTime original)
{
  var local = original.to_local();
  int year, month, day;
  local.get_ymd(out year, out month, out day);

  var timezone = local.get_timezone();
  var midnight = new DateTime(timezone, year, month, day, 0, 0, 0);

  var rand = new Rand.with_seed(get_machine_id());
  var early_hour = (TimeSpan)(rand.double_range(2, 4) * TimeSpan.HOUR);

  return midnight.add(early_hour);
}

/* Seems silly, but helpful for testing */
public TimeSpan get_day()
{
  if (in_testing_mode() && !in_unit_testing_mode())
    return TimeSpan.SECOND * (TimeSpan)10; // a day is 10s when testing
  else
    return TimeSpan.DAY;
}

// Returns when the next backup should/would be (regardless of whether auto backups are on).
// Could be in the past (if we're overdue) or in the future (if we're caught up).
// It's basically just "last backup + period + adjust to middle of night".
public DateTime next_possible_run_date()
{
  var settings = DejaDup.get_settings();

  var last_backup_string = settings.get_string(DejaDup.LAST_BACKUP_KEY);
  var last_backup = new DateTime.from_iso8601(last_backup_string, new TimeZone.utc());
  if (last_backup == null)
    return now();

  var period_days = settings.get_int(DejaDup.PERIODIC_PERIOD_KEY);
  if (period_days <= 0)
    period_days = 1;

  var period = (TimeSpan)period_days * get_day();
  return fuzz_to_midnight(last_backup.add(period));
}

public DateTime? next_run_date()
{
  var settings = DejaDup.get_settings();
  var periodic = settings.get_boolean(DejaDup.PERIODIC_KEY);

  if (!periodic)
    return null;

  return next_possible_run_date();
}

// Used not for scheduling, but to decide how dire our lack of backups is.
// Mostly (*only, at time of writing) by the monitor.
// Will not return a negative time.
public TimeSpan get_late_timespan()
{
  var next_date = next_run_date();
  if (next_date == null)
    return 0;

  var settings = DejaDup.get_settings();

  // If last backup isn't set, ignore 'next_date' because it will always be set for today,
  // but that's not how we want to think about being late. We want to look at periodic
  // timestamp in that case.
  var last_backup_string = settings.get_string(DejaDup.LAST_BACKUP_KEY);
  var last_backup = new DateTime.from_iso8601(last_backup_string, new TimeZone.utc());
  if (last_backup == null)
    next_date = null;

  var periodic_timestamp_string = settings.get_string(DejaDup.PERIODIC_TIMESTAMP_KEY);
  var periodic_timestamp = new DateTime.from_iso8601(periodic_timestamp_string, new TimeZone.utc());

  // Choose whichever event came later - our last backup or when the user enabled auto backups.
  DateTime last_event = null;
  if (next_date != null && periodic_timestamp != null) {
    last_event = periodic_timestamp.compare(next_date) > 0 ? periodic_timestamp : next_date;
  } else if (next_date != null) {
    last_event = next_date;
  } else if (periodic_timestamp != null) {
    last_event = periodic_timestamp;
  } else {
    // We've never backed up nor have a periodic timestamp!
    // Shouldn't happen because we force a periodic timestamp on startup,
    // but just for safety we'll handle it and say we're not late.
    return 0;
  }

  return int64.max(now().difference(last_event), 0);
}

// In seconds
public int get_prompt_delay()
{
  TimeSpan span = 0;
  if (DejaDup.in_testing_mode())
    span = TimeSpan.MINUTE * 2;
  else
    span = TimeSpan.DAY * 30;
  return (int)(span / TimeSpan.SECOND);
}

// This makes the check of whether we should tell user about backing up.
// For example, if a user has installed their OS and doesn't know about backing
// up, we might notify them after a month.
public bool make_prompt_check()
{
  var settings = DejaDup.get_settings();
  var prompt = settings.get_string(PROMPT_CHECK_KEY);

  if (prompt == "disabled")
    return false;
  else if (prompt == "") {
    update_prompt_time();
    return false;
  }
  else if (settings.get_string(LAST_RUN_KEY) != "")
    return false;

  // OK, monitor has run before but user hasn't yet backed up or restored.
  // Let's see whether we should prompt now.
  var last_run = new DateTime.from_iso8601(prompt, new TimeZone.utc());
  if (last_run == null)
    return false;

  last_run = last_run.add_seconds(get_prompt_delay());

  if (last_run.compare(now()) <= 0) {
    run_command({"deja-dup", "--prompt"});
    return true;
  }
  else
    return false;
}

private void update_time_key(string key, bool cancel)
{
  var settings = DejaDup.get_settings();

  if (settings.get_string(key) == "disabled")
    return; // never re-enable

  string cur_time_str;
  if (cancel) {
    cur_time_str = "disabled";
  }
  else {
    cur_time_str = current_time_as_iso8601();
  }

  settings.set_string(key, cur_time_str);
}

public void update_prompt_time(bool cancel = false)
{
  update_time_key(PROMPT_CHECK_KEY, cancel);
}

public void update_nag_time(bool cancel = false)
{
  update_time_key(NAG_CHECK_KEY, cancel);
}

// In seconds
public int get_nag_delay()
{
  TimeSpan span = 0;
  if (DejaDup.in_testing_mode())
    span = TimeSpan.MINUTE * 2;
  else
    span = TimeSpan.DAY * 30 * 2;
  return (int)(span / TimeSpan.SECOND);
}

// This makes the check of whether we should remind user about their password.
public bool is_nag_time()
{
  var settings = DejaDup.get_settings();
  var nag = settings.get_string(NAG_CHECK_KEY);
  var last_run_string = settings.get_string(LAST_BACKUP_KEY);

  if (nag == "disabled" || last_run_string == "")
    return false;
  else if (nag == "") {
    update_nag_time();
    return false;
  }

  var last_check = new DateTime.from_iso8601(nag, new TimeZone.utc());
  if (last_check == null)
    return false;

  last_check = last_check.add_seconds(get_nag_delay());

  return (last_check.compare(now()) <= 0);
}

public string process_folder_key(string folder, bool abs_allowed, out bool replaced)
{
  replaced = false;

  string processed = folder;
  if (processed.contains("$HOSTNAME")) {
    processed = processed.replace("$HOSTNAME", Environment.get_host_name());
    replaced = true;
  }

  if (!abs_allowed && processed.has_prefix("/"))
    processed = processed.substring(1);

  return processed;
}

public string get_folder_key(Settings settings, string key, bool abs_allowed = false)
{
  bool replaced;
  string folder = settings.get_string(key);
  folder = process_folder_key(folder, abs_allowed, out replaced);
  if (replaced)
    settings.set_string(key, folder);
  return folder;
}

public FilteredSettings get_settings(string? subdir = null)
{
  return new FilteredSettings(subdir);
}

public void initialize()
{
  migrate_tool_key();
  ensure_periodic_timestamp();

  /* We do a little trick here.  BackendAuto -- which is the default
     backend on a fresh install of deja-dup -- will do some work to
     automatically suss out which backend should be used instead of it.
     So we request the current backend then drop it just to get that
     ball rolling in case this is the first time. */
  DejaDup.Backend.get_default();

  // initialize network proxy, just so it can settle by the time we check it
  DejaDup.Network.get();

  // And cleanup from any previous runs
  clean_tempdirs.begin();
}

// We used to have a gsettings key that meant "put Restic files in a subdir
// called `restic/` instead of the target storage location". But to keep things
// more predictable and detectable, we transitioned to a gsettings key that
// meant "use Restic when making a fresh backup in the target storage location".
// So we need to migrate and adjust the target location if that key is set
// and from now on, we can actually trust that the location is the location.
void migrate_tool_key()
{
  var settings = get_settings();
  if (settings.get_string(_TOOL_KEY) != "restic")
    return;

  // OK the user opted into using Restic over Duplicity.
  // Let's tweak all their folder values.

  settings.set_string(_TOOL_KEY, "migrated");
  settings.set_string(TOOL_WHEN_NEW_KEY, "restic");

  migrate_tool_folder_key_helper(DRIVE_ROOT, DRIVE_FOLDER_KEY);
  migrate_tool_folder_key_helper(GOOGLE_ROOT, GOOGLE_FOLDER_KEY);
  migrate_tool_folder_key_helper(LOCAL_ROOT, LOCAL_FOLDER_KEY);
  migrate_tool_folder_key_helper(MICROSOFT_ROOT, MICROSOFT_FOLDER_KEY);
  migrate_tool_folder_key_helper(REMOTE_ROOT, REMOTE_FOLDER_KEY);
}

void migrate_tool_folder_key_helper(string root, string key)
{
  var settings = get_settings(root);
  var folder = settings.get_string(key);
  if (folder != "" && !folder.has_suffix("/"))
    folder += "/";
  folder += "restic";
  settings.set_string(key, folder);
}

// Before 49.alpha, we didn't track when the auto backup setting was changed (used
// in some "how late are we?" calculations). So set it to *now* on startup, if
// needed. (This also catches cases where the admin has set the periodic key
// enabled by default in gsettings, so we'll start counting from the first launch).
void ensure_periodic_timestamp()
{
  var settings = get_settings();

  if (settings.get_string(PERIODIC_TIMESTAMP_KEY) == "")
    settings.set_string(PERIODIC_TIMESTAMP_KEY, DejaDup.current_time_as_iso8601());
}

public void i18n_setup()
{
  var localedir = Environment.get_variable("DEJA_DUP_LOCALEDIR");
  if (localedir == null || localedir == "")
    localedir = Config.LOCALE_DIR;
  var language = Environment.get_variable("DEJA_DUP_LANGUAGE");
  if (language != null && language != "")
    Environment.set_variable("LANGUAGE", language, true);
  Intl.setlocale(LocaleCategory.ALL, "");
  Intl.textdomain(Config.GETTEXT_PACKAGE);
  Intl.bindtextdomain(Config.GETTEXT_PACKAGE, localedir);
  Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
}

public string get_file_desc(File file)
{
  if (file.is_native())
    return get_display_name(file);

  var desc = Path.get_basename(file.get_parse_name());
  try {
    var uri = Uri.parse(file.get_uri(), UriFlags.NON_DNS);
    var host = uri.get_host();
    if (host != null && host != "") {
      host = uri.get_scheme() + "://" + host;
      // translators: this is "folder-name on server-host"
      desc = _("%1$s on %2$s").printf(desc, host);
    }
  }
  catch (UriError e) {}

  return desc;
}

static File home;
static File trash;

void ensure_special_paths ()
{
  if (home == null) {
    // Fill these out for the first time
    home = File.new_for_path(Environment.get_home_dir());
    trash = File.new_for_path(InstallEnv.instance().get_trash_dir());
  }
}

public string get_display_name (File f)
{
  ensure_special_paths();

  if (f.has_prefix(home)) {
    // Unfortunately, the results of File.get_relative_path() are in local
    // encoding, not utf8, and there is no easy function to get a utf8 version.
    // So we manually convert.
    string s = home.get_relative_path(f);
    try {
      return "~/" + Filename.to_utf8(s, s.length, null, null);
    }
    catch (ConvertError e) {
      warning("%s\n", e.message);
    }
  }

  return f.get_parse_name();
}

public async string get_nickname (File f)
{
  ensure_special_paths();

  string s;
  if (f.equal(home)) {
    // Try to use the username in the display because several users have
    // previously assumed that "Home" meant "/home", and thus thought they
    // were backing up more than they were.  This should help avoid such data
    // loss accidents.
    try {
      var info = yield f.query_info_async(FileAttribute.STANDARD_DISPLAY_NAME,
                                          FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
      var username = info.get_display_name();
      if (in_demo_mode())
        username = "user";
      // Translators: this is the home folder and %s is the user's username
      s = _("Home (%s)").printf(username);
    }
    catch (Error e) {
      warning("%s\n", e.message);
      // Translators: this is the home folder
      s = _("Home");
    }
  }
  else if (f.equal(trash)) {
    // Translators: this is the trash folder
    s = _("Trash");
  }
  else
    s = DejaDup.get_display_name(f);

  return s;
}

public int get_full_backup_threshold()
{
  // So, there are a few factors affecting how often to make a fresh full
  // backup:
  //
  // 1) The longer we wait, the more we're filling up the backend with
  //    iterations on the same crap.
  // 2) The longer we wait, there's a higher risk that some bit will flip
  //    and the whole incremental chain afterwards is toast.
  // 3) The longer we wait, the less annoying we are, since full backups
  //    take a long time.
  //
  // We default to 3 months.

  var settings = get_settings();
  var threshold = settings.get_int(FULL_BACKUP_PERIOD_KEY);
  if (threshold < 0)
    threshold = 90; // 3 months
  return threshold;
}

public DateTime get_full_backup_threshold_date()
{
  var date = now();
  var days = get_full_backup_threshold();
  return date.add_days(-days);
}

public Secret.Schema get_passphrase_schema()
{
  // Use freedesktop's schema id for historical reasons
  return new Secret.Schema("org.freedesktop.Secret.Generic",
                           Secret.SchemaFlags.NONE,
                           "owner", Secret.SchemaAttributeType.STRING,
                           "type", Secret.SchemaAttributeType.STRING);
}

// Process (strips) a passphrase
public string process_passphrase(string? input)
{
  if (input == null)
    return ""; // handled the same as a null password by tools
  var processed = input.strip();
  if (processed == "") // all whitespace password?  allow it...
    return input;
  return processed;
}

// Should be called even if remember=false, so we can clear it
public async void store_passphrase(string passphrase, bool remember)
{
  try {
    if (remember && passphrase != "") {
      // Save passphrase long term
      Secret.password_store_sync(get_passphrase_schema(),
                                 Secret.COLLECTION_DEFAULT,
                                 _("Backup encryption password"),
                                 passphrase,
                                 null,
                                 "owner", Config.PACKAGE,
                                 "type", "passphrase");
    }
    else {
      // If we weren't asked to save a password, clear it out. This
      // prevents any attempt to accidentally use an old password.
      Secret.password_clear_sync(get_passphrase_schema(),
                                 null,
                                 "owner", Config.PACKAGE,
                                 "type", "passphrase");
    }
  }
  catch (Error e) {
    debug("Could not save password: %s", e.message);
  }
}

public async string? lookup_passphrase(out bool success = null)
{
  success = true;
  try {
    return Secret.password_lookup_sync(DejaDup.get_passphrase_schema(),
                                       null,
                                       "owner", Config.PACKAGE,
                                       "type", "passphrase");
  } catch (Error e) {
    warning("Could not retrieve saved password: %s", e.message);
    success = false;
    return null;
  }
}

public bool ensure_directory_exists(string path)
{
  var gfile = File.new_for_path(path);
  try {
    if (gfile.make_directory_with_parents())
      return true;
  }
  catch (IOError.EXISTS e) {
    return true; // ignore
  }
  catch (Error e) {
    warning("%s\n", e.message);
  }
  return false;
}

// Returns a new tempdir every call, on a partition with a decent amount of space.
// Dir will exist already for you.
public async string get_tempdir()
{
  var tempdir = yield find_tempdir_on_partition();

  // Adds a random namespace inside of a tempdir, so that it's easier for us to clean our own files
  // (and not hit other files, like a separately-run restic or duplicity tmp file)
  var template = Path.build_filename(tempdir, "deja-dup.XXXXXX");
  return DirUtils.mkdtemp(template);
}

// By default, duplicity uses normal tmp folders like /tmp to store its
// in-process files.  These can get quite large, especially when restoring.
// You may need up to twice the size of the largest source file.
// Because /tmp may not be super large, especially on systems that use
// tmpfs by default (e.g. Fedora 18), we try to use a tempdir that is on
// the same partition as the source files.
async string find_tempdir_on_partition()
{
  var tempdirs = get_tempdirs();

  // First, decide the "main include".  Assume that if $HOME
  // is present, that is it.  Else, the first include we find decides it.
  // This is admittedly fast and loose, but our primary concern is just
  // avoiding silly choices like tmpfs or tiny special /tmp partitions.
  var settings = get_settings();
  var include_list = settings.get_file_list(INCLUDE_LIST_KEY);
  File main_include = null;
  var home = File.new_for_path(Environment.get_home_dir());
  foreach (var include in include_list) {
    if (include.equal(home)) {
      main_include = include;
      break;
    }
    else if (main_include == null)
      main_include = include;
  }
  if (main_include == null)
    return tempdirs[0];

  // Grab that include's fs ID
  string filesystem_id;
  try {
    var info = yield main_include.query_info_async(FileAttribute.ID_FILESYSTEM,
                                                   FileQueryInfoFlags.NONE);
    filesystem_id = info.get_attribute_string(FileAttribute.ID_FILESYSTEM);
  }
  catch (Error e) {
    return tempdirs[0];
  }

  // Then, see which of our possible tempdirs matches that partition.
  foreach (var tempdir in tempdirs) {
    string temp_id;
    ensure_directory_exists(tempdir);
    try {
      var gfile = File.new_for_path(tempdir);
      var info = yield gfile.query_info_async(FileAttribute.ID_FILESYSTEM,
                                              FileQueryInfoFlags.NONE);
      temp_id = info.get_attribute_string(FileAttribute.ID_FILESYSTEM);
    }
    catch (Error e) {
      continue;
    }
    if (temp_id == filesystem_id)
      return tempdir;
  }

  // Fallback to simply using the highest preferred tempdir
  return tempdirs[0];
}

public string[] get_tempdirs()
{
  var tempdir = Environment.get_variable("DEJA_DUP_TEMPDIR");
  if (tempdir != null && tempdir != "")
    return {tempdir};

  var tempdirs = InstallEnv.instance().get_system_tempdirs();
  tempdirs += Path.build_filename(Environment.get_user_cache_dir(),
                                  Config.PACKAGE, "tmp");
  return tempdirs;
}

public async void clean_tempdirs()
{
  var tempdirs = get_tempdirs();
  const int NUM_ENUMERATED = 16;
  foreach (var tempdir in tempdirs) {
    var gfile = File.new_for_path(tempdir);

    // Now try to find and delete all files that start with "duplicity-" or "deja-dup-"
    try {
      var enumerator = yield gfile.enumerate_children_async(
                         FileAttribute.STANDARD_NAME,
                         FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
                         Priority.DEFAULT, null);
      while (true) {
        var infos = yield enumerator.next_files_async(NUM_ENUMERATED,
                                                      Priority.DEFAULT, null);
        foreach (FileInfo info in infos) {
          if (info.get_name().has_prefix("deja-dup."))
          {
            var child = gfile.get_child(info.get_name());
            yield new DejaDup.RecursiveDelete(child).start_async();
          }
        }
        if (infos.length() != NUM_ENUMERATED)
          break;
      }
    }
    catch (Error e) {
      // No worries
    }
  }
}

public string try_realpath(string input)
{
  var resolved = Posix.realpath(input);
  return resolved == null ? input : resolved;
}

// Returns a copy of the environment variables stripped of some outside
// variables that can conflict with our own configuration.
public string[] copy_env(List<string>? overrides)
{
  var builder = new StrvBuilder();

  // First, add all override and track their names
  var override_names = new GenericSet<string>(str_hash, str_equal);
  foreach (unowned var keyvar in overrides) {
    var parts = keyvar.split("=", 2);
    override_names.add(parts[0]);
    builder.add(keyvar);
  }

  // Now add all original environment variables we want to keep
  var orig_env_names = Environment.list_variables();
  for (int i = 0; orig_env_names[i] != null; i++) {
    unowned var name = orig_env_names[i];
    // Just in case the user uses restic outside of deja-dup and
    // sets env vars for it - which has been reported by users.
    // Same for rclone.
    if (!name.has_prefix("RCLONE_") &&
        !name.has_prefix("RESTIC_") &&
        !override_names.contains(name)) {
      var keyvar = "%s=%s".printf(name, Environment.get_variable(name));
      builder.add(keyvar);
    }
  }

  return builder.end();
}

// Keep a constant live reference to a single monitor. We have problems when
// we let glib manage its references, as it might kill it on us, even if we
// have open signals to it.
VolumeMonitor _monitor;
public VolumeMonitor get_volume_monitor()
{
  if (_monitor == null)
    _monitor = VolumeMonitor.get();
  return _monitor;
}

public async bool is_secret_service_available()
{
  bool success;
  yield lookup_passphrase(out success);
  return success;
}

// Block (but nicely) for a few seconds
async void wait(uint secs)
{
  Timeout.add_seconds(secs, () => {
    wait.callback();
    return Source.REMOVE;
  });
  yield;
}

} // end namespace

