diff --git a/src/AppConsole.vala b/src/AppConsole.vala index e0983ccc..54c2006d 100644 --- a/src/AppConsole.vala +++ b/src/AppConsole.vala @@ -1355,6 +1355,10 @@ public class AppConsole : GLib.Object { public bool delete_snapshot(){ + if (App.btrfs_mode && (App.check_btrfs_layout_system() == false)){ + return false; + } + select_snapshot_device(true); select_snapshot_for_deletion(); @@ -1367,6 +1371,10 @@ public class AppConsole : GLib.Object { } public bool delete_all_snapshots(){ + + if (App.btrfs_mode && (App.check_btrfs_layout_system() == false)){ + return false; + } select_snapshot_device(true); diff --git a/src/Core/Main.vala b/src/Core/Main.vala index 713ff4fd..6e138889 100644 --- a/src/Core/Main.vala +++ b/src/Core/Main.vala @@ -53,6 +53,9 @@ public class Main : GLib.Object{ public bool include_btrfs_home_for_backup = false; public bool include_btrfs_home_for_restore = false; public static bool btrfs_version__can_recursive_delete = false; + + public string root_subvolume_name = "@"; + public string home_subvolume_name = "@home"; public bool stop_cron_emails = true; @@ -289,6 +292,23 @@ public class Main : GLib.Object{ this.app_conf_path_old = "/etc/timeshift.json"; this.app_conf_path_default = GLib.Path.build_path (GLib.Path.DIR_SEPARATOR_S, Constants.SYSCONFDIR, "timeshift", "default.json"); //sys_root and sys_home will be initialized by update_partition_list() + + // Detect subvolume names based on distro id. + // Only has effect when timeshift is opened the first time, + // otherwise the setting is overwritten by loading the config. + if (this.current_distro.dist_id.down() == "fedora") { + this.root_subvolume_name = "root"; + this.home_subvolume_name = "home"; + } + else if (this.current_distro.dist_id.down() == "debian") { + this.root_subvolume_name = "@rootfs"; + this.home_subvolume_name = ""; + this.include_btrfs_home_for_backup = false; + } + else { //if (this.current_distro.dist_id.down() == "ubuntu") + this.root_subvolume_name = "@"; + this.home_subvolume_name = "@home"; + } // check if running locally ------------------------ @@ -483,34 +503,85 @@ public class Main : GLib.Object{ log_msg(_("** Uninstalled Timeshift BTRFS **")); } } - - public bool check_btrfs_layout_system(Gtk.Window? win = null){ - log_debug("check_btrfs_layout_system()"); + /* + * Checks if root_subvolume_name and home_subvolume_name are configured to valid values. + * + * @return False if the config is invalid. + */ + public bool check_btrfs_system_config(out string title, out string msg) { - bool supported = sys_subvolumes.has_key("@"); - if (include_btrfs_home_for_backup){ - supported = supported && sys_subvolumes.has_key("@home"); + log_debug("check_btrfs_system_config()"); + + // If the root subvolume is configured to an empty string, the config is invalid. + if(root_subvolume_name == "") { + title = _("Root subvolume configuration is invalid"); + msg = _("Root subvolume name is empty, make sure to select a valid subvolume layout."); + return false; } - if (!supported){ - string msg = _("The system partition has an unsupported subvolume layout.") + " "; - msg += _("Only ubuntu-type layouts with @ and @home subvolumes are currently supported.") + "\n\n"; - msg += _("Application will exit.") + "\n\n"; - string title = _("Not Supported"); - + // If the home subvolume is configured to an empty string and home backup is + // enabbled, the config is invalid. + if (include_btrfs_home_for_backup && home_subvolume_name == "") { + title = _("Home subvolume configuration is invalid"); + msg = _("Home backups are enabled home subvolume name is empty, make sure to select a valid subvolume layout."); + return false; + } + + // If sys_subvolumes does not contain a subvolume for root_subvolume_name, + // the config is invalid. + if (!sys_subvolumes.has_key(root_subvolume_name)) { + title = _("Root subvolume configuration is invalid"); + msg = _("The configured root subvolume does not exist") + " (" + root_subvolume_name + ")."; + return false; + } + + // If home backups are enbaled and sys_subvolumes does not contain a subvolume + // for home_subvolume_name, the config is invalid. + if (include_btrfs_home_for_backup && !sys_subvolumes.has_key(home_subvolume_name)) { + title = _("Home subvolume configuration is invalid"); + msg = _("Home backups are enabled and the configured home subvolume does not exist") + " (" + home_subvolume_name + ")."; + return false; + } + + return true; + } + + /* + * Calls check_btrfs_system_config and displays an error message to the user + * + * @return False if the config is invalid. + */ + public bool check_btrfs_layout_system(Gtk.Window? win = null) { + + log_debug("check_btrfs_layout_system()"); + + // Checking for failure conditions + string title; + string msg; + if(!check_btrfs_system_config(out title, out msg)) { if (app_mode == ""){ gtk_set_busy(false, win); gtk_messagebox(title, msg, win, true); } else{ - log_error(msg); + msg += "\n\n" + _("Application will exit.") + "\n\n"; + log_error(title + "\n\n" + msg); } + + return false; } - return supported; + return true; } + /* + * Checks if the root and home devices are btrfs filesystems and does some + * further checking if the device fs contains the requested suvolume names + * with check_btrfs_volume(). + * + * @return True if the layout is supported. + */ public bool check_btrfs_layout(Device? dev_root, Device? dev_home, bool unlock){ bool supported = true; // keep true for non-btrfs systems @@ -521,18 +592,18 @@ public class Main : GLib.Object{ if (dev_home != dev_root){ - supported = supported && check_btrfs_volume(dev_root, "@", unlock); + supported = supported && check_btrfs_volume(dev_root, {root_subvolume_name}, unlock); if (include_btrfs_home_for_backup){ - supported = supported && check_btrfs_volume(dev_home, "@home", unlock); + supported = supported && check_btrfs_volume(dev_home, {home_subvolume_name}, unlock); } } else{ if (include_btrfs_home_for_backup){ - supported = supported && check_btrfs_volume(dev_root, "@,@home", unlock); + supported = supported && check_btrfs_volume(dev_root, {root_subvolume_name, home_subvolume_name}, unlock); } else{ - supported = supported && check_btrfs_volume(dev_root, "@", unlock); + supported = supported && check_btrfs_volume(dev_root, {root_subvolume_name}, unlock); } } } @@ -1720,9 +1791,9 @@ public class Main : GLib.Object{ log_msg(_("Creating new backup...") + "(BTRFS)"); - log_msg(_("Saving to device") + ": %s".printf(repo.device.device) + ", " + _("mounted at path") + ": %s".printf(repo.mount_paths["@"])); + log_msg(_("Saving to device") + ": %s".printf(repo.device.device) + ", " + _("mounted at path") + ": %s".printf(repo.mount_paths[root_subvolume_name])); if ((repo.device_home != null) && (repo.device_home.uuid != repo.device.uuid)){ - log_msg(_("Saving to device") + ": %s".printf(repo.device_home.device) + ", " + _("mounted at path") + ": %s".printf(repo.mount_paths["@home"])); + log_msg(_("Saving to device") + ": %s".printf(repo.device_home.device) + ", " + _("mounted at path") + ": %s".printf(repo.mount_paths[home_subvolume_name])); } // take new backup --------------------------------- @@ -1739,11 +1810,11 @@ public class Main : GLib.Object{ // create subvolume snapshots - var subvol_names = new string[] { "@" }; + var subvol_names = new string[] { root_subvolume_name }; if (include_btrfs_home_for_backup){ - subvol_names = new string[] { "@","@home" }; + subvol_names = new string[] { root_subvolume_name,home_subvolume_name }; } foreach(var subvol_name in subvol_names){ @@ -1757,11 +1828,13 @@ public class Main : GLib.Object{ string dst_path = path_combine(snapshot_path, subvol_name); // Dirty hack to fix the nested subvilumes issue (cause of issue is unknown) - if (dst_path.has_suffix("/@/@")){ - dst_path = dst_path.replace("/@/@", "/@"); + string nested_root_subvol = @"/$(root_subvolume_name)/$(root_subvolume_name)"; + string nested_home_subvol = @"/$(home_subvolume_name)/$(home_subvolume_name)"; + if (dst_path.has_suffix(nested_root_subvol)){ + dst_path = dst_path.replace(nested_root_subvol, @"/$(root_subvolume_name)"); } - else if (dst_path.has_suffix("/@home/@home")){ - dst_path = dst_path.replace("/@home/@home", "/@home"); + else if (dst_path.has_suffix(nested_home_subvol)){ + dst_path = dst_path.replace(nested_home_subvol, @"/$(home_subvolume_name)"); } string cmd = "btrfs subvolume snapshot '%s' '%s' \n".printf(src_path, dst_path); @@ -1786,7 +1859,7 @@ public class Main : GLib.Object{ //log_msg(_("Writing control file...")); - snapshot_path = path_combine(repo.mount_paths["@"], "timeshift-btrfs/snapshots/%s".printf(snapshot_name)); + snapshot_path = path_combine(repo.mount_paths[root_subvolume_name], "timeshift-btrfs/snapshots/%s".printf(snapshot_name)); string initial_tags = (tag == "ondemand") ? "" : tag; @@ -2300,6 +2373,10 @@ public class Main : GLib.Object{ log_debug("Main: restore_snapshot()"); + if (btrfs_mode && (check_btrfs_layout_system() == false)){ + return false; + } + parent_window = parent_win; // remove mount points which will remain on root fs @@ -2344,12 +2421,12 @@ public class Main : GLib.Object{ // final check - check if target root device is mounted if (btrfs_mode){ - if (repo.mount_paths["@"].length == 0){ - log_error(_("BTRFS device is not mounted") + ": @"); + if (repo.mount_paths[root_subvolume_name].length == 0){ + log_error(_("BTRFS device is not mounted") + ": " + root_subvolume_name); return false; } - if (include_btrfs_home_for_restore && (repo.mount_paths["@home"].length == 0)){ - log_error(_("BTRFS device is not mounted") + ": @home"); + if (include_btrfs_home_for_restore && (repo.mount_paths[home_subvolume_name].length == 0)){ + log_error(_("BTRFS device is not mounted") + ": " + home_subvolume_name); return false; } } @@ -2430,7 +2507,7 @@ public class Main : GLib.Object{ if (!App.snapshot_to_restore.subvolumes.has_key(entry.subvolume_name())){ continue; } - if ((entry.subvolume_name() == "@home") && !include_btrfs_home_for_restore){ continue; } + if ((entry.subvolume_name() == home_subvolume_name) && !include_btrfs_home_for_restore){ continue; } } string dev_name = entry.device.full_name_with_parent; @@ -2466,7 +2543,7 @@ public class Main : GLib.Object{ if (!App.snapshot_to_restore.subvolumes.has_key(entry.subvolume_name())){ continue; } - if ((entry.subvolume_name() == "@home") && !include_btrfs_home_for_restore){ continue; } + if ((entry.subvolume_name() == home_subvolume_name) && !include_btrfs_home_for_restore){ continue; } } string dev_name = entry.device.full_name_with_parent; @@ -3166,7 +3243,7 @@ public class Main : GLib.Object{ foreach(var subvol in snapshot_to_restore.subvolumes.values){ - if ((subvol.name == "@home") && !include_btrfs_home_for_restore){ continue; } + if ((subvol.name == home_subvolume_name) && !include_btrfs_home_for_restore){ continue; } subvol.restore(); } @@ -3206,9 +3283,9 @@ public class Main : GLib.Object{ string snapshot_path = ""; /* Note: - * The @ and @home subvolumes need to be backed-up only if they are in use by the system. + * The root and home subvolumes need to be backed-up only if they are in use by the system. * If user restores a snapshot and then tries to restore another snapshot before the next reboot - * then the @ and @home subvolumes are the ones that were previously restored and need to be deleted. + * then the root and home subvolumes are the ones that were previously restored and need to be deleted. * */ bool create_pre_restore_backup = false; @@ -3230,13 +3307,13 @@ public class Main : GLib.Object{ if (found){ //delete system subvolumes - if (sys_subvolumes.has_key("@") && snapshot_to_restore.subvolumes.has_key("@")){ - sys_subvolumes["@"].remove(); - log_msg(_("Deleted subvolume") + ": @"); + if (sys_subvolumes.has_key(root_subvolume_name) && snapshot_to_restore.subvolumes.has_key(root_subvolume_name)){ + sys_subvolumes[root_subvolume_name].remove(); + log_msg(_("Deleted subvolume") + ": " + root_subvolume_name); } - if (include_btrfs_home_for_restore && sys_subvolumes.has_key("@home") && snapshot_to_restore.subvolumes.has_key("@home")){ - sys_subvolumes["@home"].remove(); - log_msg(_("Deleted subvolume") + ": @home"); + if (include_btrfs_home_for_restore && sys_subvolumes.has_key(home_subvolume_name) && snapshot_to_restore.subvolumes.has_key(home_subvolume_name)){ + sys_subvolumes[home_subvolume_name].remove(); + log_msg(_("Deleted subvolume") + ": " + home_subvolume_name); } //update description for pre-restore backup @@ -3263,9 +3340,9 @@ public class Main : GLib.Object{ var subvol_list = new Gee.ArrayList(); - var subvol_names = new string[] { "@" }; + var subvol_names = new string[] { root_subvolume_name }; if (include_btrfs_home_for_restore){ - subvol_names = new string[] { "@","@home" }; + subvol_names = new string[] { root_subvolume_name,home_subvolume_name }; } foreach(string subvol_name in subvol_names){ @@ -3294,7 +3371,7 @@ public class Main : GLib.Object{ return false; } else{ - var subvol_dev = (subvol_name == "@") ? repo.device : repo.device_home; + var subvol_dev = (subvol_name == root_subvolume_name) ? repo.device : repo.device_home; subvol_list.add(new Subvolume(subvol_name, dst_path, subvol_dev.uuid, repo)); log_msg(_("Moved system subvolume to snapshot directory") + ": %s".printf(subvol_name)); @@ -3308,11 +3385,11 @@ public class Main : GLib.Object{ else{ // write control file ----------- - snapshot_path = path_combine(repo.mount_paths["@"], "timeshift-btrfs/snapshots/%s".printf(snapshot_name)); + snapshot_path = path_combine(repo.mount_paths[root_subvolume_name], "timeshift-btrfs/snapshots/%s".printf(snapshot_name)); var snap = Snapshot.write_control_file( snapshot_path, dt_created, repo.device.uuid, - LinuxDistro.get_dist_info(path_combine(snapshot_path,"@")).full_name(), + LinuxDistro.get_dist_info(path_combine(snapshot_path,root_subvolume_name)).full_name(), "ondemand", "", 0, true, false, repo); snap.description = "Before restoring '%s'".printf(snapshot_to_restore.date_formatted); @@ -3357,6 +3434,9 @@ public class Main : GLib.Object{ config.set_string_member("parent_device_uuid", backup_parent_uuid); } + config.set_string_member("root_subvolume_name", root_subvolume_name); + config.set_string_member("home_subvolume_name", home_subvolume_name); + config.set_string_member("do_first_run", false.to_string()); config.set_string_member("btrfs_mode", btrfs_mode.to_string()); config.set_string_member("include_btrfs_home_for_backup", include_btrfs_home_for_backup.to_string()); @@ -3476,6 +3556,9 @@ public class Main : GLib.Object{ if (cmd_btrfs_mode != null){ btrfs_mode = cmd_btrfs_mode; //override } + + root_subvolume_name = json_get_string(config,"root_subvolume_name", root_subvolume_name); + home_subvolume_name = json_get_string(config,"home_subvolume_name", home_subvolume_name); backup_uuid = json_get_string(config,"backup_device_uuid", backup_uuid); backup_parent_uuid = json_get_string(config,"parent_device_uuid", backup_parent_uuid); @@ -3544,7 +3627,7 @@ public class Main : GLib.Object{ // load some defaults for first-run based on user's system type - bool supported = sys_subvolumes.has_key("@") && cmd_exists("btrfs"); // && sys_subvolumes.has_key("@home") + bool supported = sys_subvolumes.has_key(root_subvolume_name) && cmd_exists("btrfs"); // && sys_subvolumes.has_key(home_subvolume_name) if (supported || (cmd_btrfs_mode == true)){ log_msg(_("Selected default snapshot type") + ": %s".printf("BTRFS")); btrfs_mode = true; @@ -3830,11 +3913,11 @@ public class Main : GLib.Object{ if (mnt.device.fstype == "btrfs"){ if (mnt.mount_point == "/"){ - mount_options = "subvol=@"; + mount_options = "subvol=" + root_subvolume_name; } if (include_btrfs_home_for_restore){ if (mnt.mount_point == "/home"){ - mount_options = "subvol=@home"; + mount_options = "subvol=" + home_subvolume_name; } } } @@ -3884,9 +3967,18 @@ public class Main : GLib.Object{ return repo.status_code; } - public bool check_btrfs_volume(Device dev, string subvol_names, bool unlock){ + /* + * Checks if device has subvolumes listed in subvol_names. + * Mounts the device if it's not mounted. Device is unmounted in the end. + * + * @return True if device has subvolumes. + */ + public bool check_btrfs_volume(Device dev, string[] subvol_names, bool unlock){ - log_debug("check_btrfs_volume():%s".printf(subvol_names)); + log_debug("check_btrfs_volume()"); + foreach(string subvol_name in subvol_names) { + log_debug("-- " + subvol_name); + } string mnt_btrfs = mount_point_app + "/btrfs"; dir_create(mnt_btrfs); @@ -3906,7 +3998,7 @@ public class Main : GLib.Object{ if (dev_unlocked == null){ log_debug("device is null"); - log_debug("SnapshotRepo: check_btrfs_volume(): exit"); + log_debug("Main: check_btrfs_volume(): exit"); return false; } else{ @@ -3924,7 +4016,7 @@ public class Main : GLib.Object{ bool supported = true; - foreach(string subvol_name in subvol_names.split(",")){ + foreach(string subvol_name in subvol_names){ supported = supported && dir_exists(path_combine(mnt_btrfs,subvol_name)); } @@ -3959,8 +4051,8 @@ public class Main : GLib.Object{ update_partitions(); // In BTRFS mode, select the system disk if system disk is BTRFS - if (btrfs_mode && sys_subvolumes.has_key("@")){ - var subvol_root = sys_subvolumes["@"]; + if (btrfs_mode && sys_subvolumes.has_key(root_subvolume_name)){ + var subvol_root = sys_subvolumes[root_subvolume_name]; repo = new SnapshotRepo.from_device(subvol_root.get_device(), parent_win, btrfs_mode); return; } @@ -3983,7 +4075,7 @@ public class Main : GLib.Object{ if (dev.has_children()) { return false; } if (btrfs_mode && ((dev.fstype == "btrfs")||(dev.fstype == "luks"))){ - if (check_btrfs_volume(dev, "@", unlock)){ + if (check_btrfs_volume(dev, {root_subvolume_name}, unlock)){ return true; } } @@ -4109,16 +4201,28 @@ public class Main : GLib.Object{ } public bool query_subvolume_ids(){ - bool ok = query_subvolume_id("@"); + bool ok = query_subvolume_id(root_subvolume_name); if ((repo.device_home != null) && (repo.device.uuid != repo.device_home.uuid)){ - ok = ok && query_subvolume_id("@home"); + ok = ok && query_subvolume_id(home_subvolume_name); } return ok; } + /** + * Queries the subvolume ID of a given subvolume by name. + * + * Subvolumes are listed using ``btrfs subvolume list``. + * This assigns the ID to the subvolume found in ``sys_subvolumes`` or + * ``repo.snapshots``. + * + * @return true if no error occured. + */ public bool query_subvolume_id(string subvol_name){ - log_debug("query_subvolume_id():%s".printf(subvol_name)); + log_debug("query_subvolume_id(): \"%s\"".printf(subvol_name)); + + // Check validity of arguments, querying empty string is an error. + if (subvol_name == "") return false; string cmd = ""; string std_out; @@ -4149,15 +4253,23 @@ public class Main : GLib.Object{ Subvolume subvol = null; - if ((sys_subvolumes.size > 0) && line.has_suffix(sys_subvolumes["@"].path.replace(repo.mount_paths["@"] + "/"," "))){ - subvol = sys_subvolumes["@"]; + // Is the subvolume we are trying to ID the root subvolume? + if ((sys_subvolumes.size > 0) + && (root_subvolume_name != "") + && sys_subvolumes.has_key(root_subvolume_name) + && line.has_suffix(sys_subvolumes[root_subvolume_name].path.replace(repo.mount_paths[root_subvolume_name] + "/"," "))){ + + subvol = sys_subvolumes[root_subvolume_name]; } + // Or is it the home subvolume? else if ((sys_subvolumes.size > 0) - && sys_subvolumes.has_key("@home") - && line.has_suffix(sys_subvolumes["@home"].path.replace(repo.mount_paths["@home"] + "/"," "))){ + && (home_subvolume_name != "") + && sys_subvolumes.has_key(home_subvolume_name) + && line.has_suffix(sys_subvolumes[home_subvolume_name].path.replace(repo.mount_paths[home_subvolume_name] + "/"," "))){ - subvol = sys_subvolumes["@home"]; + subvol = sys_subvolumes[home_subvolume_name]; } + // Otherwise, can we find the subvolume in snapshts? else { foreach(var bak in repo.snapshots){ foreach(var sub in bak.subvolumes.values){ @@ -4169,6 +4281,7 @@ public class Main : GLib.Object{ } } + // Assign the subvolume ID to the found subvolume. if (subvol != null){ subvol.id = long.parse(parts[1]); } @@ -4178,15 +4291,27 @@ public class Main : GLib.Object{ } public bool query_subvolume_quotas(){ - bool ok = query_subvolume_quota("@"); + bool ok = query_subvolume_quota(root_subvolume_name); if (repo.device.uuid != repo.device_home.uuid){ - ok = ok && query_subvolume_quota("@home"); + ok = ok && query_subvolume_quota(home_subvolume_name); } return ok; } + /** + * Queries the subvolume quotas of a given subvolume by name. + * + * Subvolumes qoatas are listed using ``btrfs qgroup show``. + * This function assigns the quotas to the subvolume found in ``sys_subvolumes`` + * or ``repo.snapshots``. + * + * @return true if no error occured. + */ public bool query_subvolume_quota(string subvol_name){ - log_debug("query_subvolume_quota():%s".printf(subvol_name)); + log_debug("query_subvolume_quota(): \"%s\"".printf(subvol_name)); + + // Check validity of arguments, querying empty string is an error. + if (subvol_name == "") return false; string cmd = ""; string std_out; @@ -4241,16 +4366,23 @@ public class Main : GLib.Object{ Subvolume subvol = null; - if ((sys_subvolumes.size > 0) && (sys_subvolumes["@"].id == subvol_id)){ + // Is the subvolume we are trying to get quotas for the root subvolume? + if ((sys_subvolumes.size > 0) + && (root_subvolume_name != "") + && sys_subvolumes.has_key(root_subvolume_name) + && (sys_subvolumes[root_subvolume_name].id == subvol_id)){ - subvol = sys_subvolumes["@"]; + subvol = sys_subvolumes[root_subvolume_name]; } + // Or is it the home subvolume? else if ((sys_subvolumes.size > 0) - && sys_subvolumes.has_key("@home") - && (sys_subvolumes["@home"].id == subvol_id)){ + && (home_subvolume_name != "") + && sys_subvolumes.has_key(home_subvolume_name) + && (sys_subvolumes[home_subvolume_name].id == subvol_id)){ - subvol = sys_subvolumes["@home"]; + subvol = sys_subvolumes[home_subvolume_name]; } + // Otherwise, can we find the subvolume in snapshts? else { foreach(var bak in repo.snapshots){ foreach(var sub in bak.subvolumes.values){ @@ -4261,6 +4393,7 @@ public class Main : GLib.Object{ } } + // Assign the subvolume quotas to the found subvolume. if (subvol != null){ int part_num = -1; foreach(string part in parts){ diff --git a/src/Core/Snapshot.vala b/src/Core/Snapshot.vala index 4898925c..a37758c4 100644 --- a/src/Core/Snapshot.vala +++ b/src/Core/Snapshot.vala @@ -228,7 +228,7 @@ public class Snapshot : GLib.Object{ live = json_get_bool(config,"live",false); string type = config.get_string_member_with_default("type", "rsync"); - string extension = (type == "btrfs") ? "@" : "localhost"; + string extension = (type == "btrfs") ? App.root_subvolume_name : "localhost"; distro = LinuxDistro.get_dist_info(path_combine(path, extension)); //log_debug("repo.mount_path: %s".printf(repo.mount_path)); @@ -239,7 +239,7 @@ public class Snapshot : GLib.Object{ foreach(string subvol_name in subvols.get_members()){ - if ((subvol_name != "@")&&(subvol_name != "@home")){ continue; } + if ((subvol_name != App.root_subvolume_name)&&(subvol_name != App.home_subvolume_name)){ continue; } paths[subvol_name] = path.replace(repo.mount_path, repo.mount_paths[subvol_name]); @@ -321,7 +321,7 @@ public class Snapshot : GLib.Object{ string fstab_path = path_combine(path, "/localhost/etc/fstab"); if (btrfs_mode){ - fstab_path = path_combine(path, "/@/etc/fstab"); + fstab_path = path_combine(path, @"/$(App.root_subvolume_name)/etc/fstab"); } fstab_list = FsTabEntry.read_file(fstab_path); @@ -332,7 +332,7 @@ public class Snapshot : GLib.Object{ string crypttab_path = path_combine(path, "/localhost/etc/crypttab"); if (btrfs_mode){ - crypttab_path = path_combine(path, "/@/etc/crypttab"); + crypttab_path = path_combine(path, @"/$(App.root_subvolume_name)/etc/crypttab"); } cryttab_list = CryptTabEntry.read_file(crypttab_path); @@ -439,7 +439,7 @@ public class Snapshot : GLib.Object{ public bool has_subvolumes(){ foreach(FsTabEntry en in fstab_list){ - if (en.options.contains("subvol=@")){ + if (en.options.contains("subvol=" + App.root_subvolume_name)){ return true; } } diff --git a/src/Core/SnapshotRepo.vala b/src/Core/SnapshotRepo.vala index 5be72936..a7203a94 100644 --- a/src/Core/SnapshotRepo.vala +++ b/src/Core/SnapshotRepo.vala @@ -191,33 +191,33 @@ public class SnapshotRepo : GLib.Object{ if (mount_path.length == 0){ return false; } - - // rsync - mount_paths["@"] = ""; - mount_paths["@home"] = ""; if (btrfs_mode){ - - mount_paths["@"] = mount_path; - mount_paths["@home"] = mount_path; //default + + if (App.root_subvolume_name == "") return false; + + // Don't add mount paths if root_subvolume_name or home_subvolume_name are empty. + // We don't add default values either anymore, because it could lead us to bugs. + mount_paths[App.root_subvolume_name] = mount_path; + if (App.home_subvolume_name != "") mount_paths[App.home_subvolume_name] = mount_path; device_home = device; //default // mount @home if on different disk ------- - var repo_subvolumes = Subvolume.detect_subvolumes_for_system_by_path(path_combine(mount_path,"@"), this, parent_window); + var repo_subvolumes = Subvolume.detect_subvolumes_for_system_by_path(path_combine(mount_path,App.root_subvolume_name), this, parent_window); - if (repo_subvolumes.has_key("@home")){ + if (App.home_subvolume_name != "" && repo_subvolumes.has_key(App.home_subvolume_name)){ - var subvol = repo_subvolumes["@home"]; + var subvol = repo_subvolumes[App.home_subvolume_name]; if (subvol.device_uuid != device.uuid){ // @home is on a separate device device_home = subvol.get_device(); - mount_paths["@home"] = unlock_and_mount_device(device_home, App.mount_point_app + "/backup-home"); + mount_paths[App.home_subvolume_name] = unlock_and_mount_device(device_home, App.mount_point_app + "/backup-home"); - if (mount_paths["@home"].length == 0){ + if (mount_paths[App.home_subvolume_name].length == 0){ return false; } } @@ -469,51 +469,96 @@ public class SnapshotRepo : GLib.Object{ //log_debug("checking selected device"); + // Snapshot repo is available if the config is valid. + bool ok = check_config(out status_message, out status_details, out status_code); + + if (ok){ + log_debug(status_message); + log_debug("is_available: ok"); + } else { + log_debug(status_message); + log_debug("is_available: false"); + } + + return ok; + } + + /* + * Validates the btrfs config and displays an appropriate error message for the user. + * It's a little convoluted because it needs to catch any misconfiguration leading + * to error states. + */ + private bool check_config(out string title, out string msg, out SnapshotLocationStatus code){ + + log_debug("SnapshotRepo: check_btrfs_config()"); + + log_debug("btrfs_mode=%s".printf(btrfs_mode.to_string())); + if (device == null){ if (App.backup_uuid == null || App.backup_uuid.length == 0){ log_debug("device is null"); - status_message = _("Snapshot device not selected"); - status_details = _("Select the snapshot device"); - status_code = SnapshotLocationStatus.NOT_SELECTED; - log_debug("is_available: false"); + title = _("Snapshot device not selected"); + msg = _("Select the snapshot device"); + code = SnapshotLocationStatus.NOT_SELECTED; return false; } else{ - status_message = _("Snapshot device not available"); - status_details = _("Device not found") + ": UUID='%s'".printf(App.backup_uuid); - status_code = SnapshotLocationStatus.NOT_AVAILABLE; - log_debug("is_available: false"); + title = _("Snapshot device not available"); + msg = _("Device not found") + ": UUID='%s'".printf(App.backup_uuid); + code = SnapshotLocationStatus.NOT_AVAILABLE; return false; } } - else{ - if (btrfs_mode){ - bool ok = has_btrfs_system(); - if (ok){ - log_debug("is_available: ok"); - } - return ok; - } - else{ - log_debug("is_available: ok"); - return true; + + if (btrfs_mode) { + // Run the btrfs checks from Main.check_btrfs_system_config. + if (!App.check_btrfs_system_config(out title, out msg)) { + code = SnapshotLocationStatus.NO_BTRFS_SYSTEM; + return false; } - } - } - public bool has_btrfs_system(){ - - log_debug("SnapshotRepo: has_btrfs_system()"); + // Run some additional checks, these cases are unlikely to come up, + // but they could point to bugs in SnapshotRepo code or any other + // code related to configuring the system layout. + // Bear with me, even though it seems confusing at first... - var root_path = path_combine(mount_paths["@"],"@"); - log_debug("root_path=%s".printf(root_path)); - log_debug("btrfs_mode=%s".printf(btrfs_mode.to_string())); - if (btrfs_mode){ - if (!dir_exists(root_path)){ - status_message = _("Selected snapshot device is not a system disk"); - status_details = _("Select BTRFS system disk with root subvolume (@)"); - status_code = SnapshotLocationStatus.NO_BTRFS_SYSTEM; - log_debug(status_message); + // Check the home subvolume configuration. + // home_path can be null if mount_paths[App.home_subvolume_name] doesn't exist. + var home_path = path_combine(mount_paths[App.home_subvolume_name], App.home_subvolume_name); + var home_subvolume_configured = App.home_subvolume_name != ""; + var has_home_mount_path = mount_paths.has_key(App.home_subvolume_name); + + log_debug("home_path=%s".printf(home_path)); + log_debug("home_subvolume_configured=%s".printf(home_subvolume_configured.to_string())); + log_debug("has_home_mount_path=%s".printf(has_home_mount_path.to_string())); + + // If home_subvolume_configured and the directory does not exists, the + // configuration is invalid. + if (!has_home_mount_path || (home_subvolume_configured && !dir_exists(home_path))) { + title = _("Home subvolume configuration is invalid"); + msg = _("Home subvolume is configured but the path does not exist") + " (" + App.home_subvolume_name + ")"; + code = SnapshotLocationStatus.NO_BTRFS_SYSTEM; + return false; + } + + // Further check root subvolume configuration. + // We already know App.root_subvolume_name is not empty, but we still need to check if + // the key exists (it might not if there is no mount path associated to it). + var has_root_mount_path = mount_paths.has_key(App.root_subvolume_name); + // root_path can still be null if has_root_mount_path is false. + var root_path = path_combine(mount_paths[App.root_subvolume_name], App.root_subvolume_name); + + log_debug("root_path=%s".printf(root_path)); + log_debug("has_root_mount_path=%s".printf(has_root_mount_path.to_string())); + + // If we don't have a mount_path for root, or root_path is null, or the root_path + // directory doesn't exist, the configuration is invalid. + // Technically, we don't need to explicitly check root_path here, it's implied by + // !has_root_mount_path. + if (!has_root_mount_path || !dir_exists(root_path)){ + title = _("Root subvolume configuration is invalid"); + msg = _("Root subvolume is configured but the path does not exist. Select BTRFS system disk with root subvolume ") + " (" + App.home_subvolume_name + ")"; + code = SnapshotLocationStatus.NO_BTRFS_SYSTEM; return false; } } diff --git a/src/Core/Subvolume.vala b/src/Core/Subvolume.vala index 1520a1b1..045005aa 100644 --- a/src/Core/Subvolume.vala +++ b/src/Core/Subvolume.vala @@ -124,11 +124,11 @@ public class Subvolume : GLib.Object{ public bool remove(){ if (is_system_subvolume){ - if (name == "@"){ - path = path_combine(App.mount_point_app + "/backup", "@"); + if (name == App.root_subvolume_name){ + path = path_combine(App.mount_point_app + "/backup", App.root_subvolume_name); } - else if (name == "@home"){ - path = path_combine(App.mount_point_app + "/backup-home", "@home"); + else if (name == App.home_subvolume_name){ + path = path_combine(App.mount_point_app + "/backup-home", App.home_subvolume_name); } } diff --git a/src/Gtk/MainWindow.vala b/src/Gtk/MainWindow.vala index cd127b1d..535cd4ec 100644 --- a/src/Gtk/MainWindow.vala +++ b/src/Gtk/MainWindow.vala @@ -760,6 +760,13 @@ class MainWindow : Gtk.Window{ private void restore(){ + + ui_sensitive(false); + + if (App.btrfs_mode && (App.check_btrfs_layout_system(this) == false)){ + ui_sensitive(true); + return; + } TreeIter iter; TreeSelection sel; @@ -827,6 +834,7 @@ class MainWindow : Gtk.Window{ App.dry_run = false; App.repo.load_snapshots(); refresh_all(); + ui_sensitive(true); }); } diff --git a/src/Gtk/RestoreWindow.vala b/src/Gtk/RestoreWindow.vala index b88f4bd2..64e536cd 100644 --- a/src/Gtk/RestoreWindow.vala +++ b/src/Gtk/RestoreWindow.vala @@ -268,7 +268,7 @@ class RestoreWindow : Gtk.Window{ if (App.btrfs_mode){ - if (App.snapshot_to_restore.subvolumes.has_key("@home")){ + if (App.snapshot_to_restore.subvolumes.has_key(App.home_subvolume_name)){ notebook.page = Tabs.USERS; } diff --git a/src/Gtk/SnapshotBackendBox.vala b/src/Gtk/SnapshotBackendBox.vala index 9df91918..1cdb5e8d 100644 --- a/src/Gtk/SnapshotBackendBox.vala +++ b/src/Gtk/SnapshotBackendBox.vala @@ -37,7 +37,9 @@ class SnapshotBackendBox : Gtk.Box{ private Gtk.RadioButton opt_rsync; private Gtk.RadioButton opt_btrfs; private Gtk.Label lbl_description; + private Gtk.ComboBox combo_subvol_layout; private Gtk.Window parent_window; + private Gtk.Box vbox_subvolume_custom; public signal void type_changed(); @@ -82,6 +84,7 @@ class SnapshotBackendBox : Gtk.Box{ opt_rsync.toggled.connect(()=>{ if (opt_rsync.active){ App.btrfs_mode = false; + combo_subvol_layout.sensitive = false; Main.first_snapshot_size = 0; init_backend(); type_changed(); @@ -97,6 +100,8 @@ class SnapshotBackendBox : Gtk.Box{ hbox.add (opt); opt_btrfs = opt; + create_btrfs_subvolume_selection(hbox); + if (!check_for_btrfs_tools()) { opt.sensitive = false; opt_rsync.active = true; @@ -105,6 +110,7 @@ class SnapshotBackendBox : Gtk.Box{ opt_btrfs.toggled.connect(()=>{ if (opt_btrfs.active){ App.btrfs_mode = true; + combo_subvol_layout.sensitive = true; init_backend(); type_changed(); update_description(); @@ -112,6 +118,159 @@ class SnapshotBackendBox : Gtk.Box{ }); } + /** + * Creates BTRFS subvolume selection UI + */ + private void create_btrfs_subvolume_selection(Gtk.Box vbox) { + + // subvolume layout + var hbox_subvolume = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + vbox.add(hbox_subvolume); + + var sg_label = new Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL); + var sg_edit = new Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL); + + var lbl_subvol_name = new Gtk.Label(_("Subvolume layout:")); + lbl_subvol_name.xalign = (float) 0.0; + hbox_subvolume.add(lbl_subvol_name); + sg_label.add_widget(lbl_subvol_name); + + // Combobox + var layout = new string[]{ + App.root_subvolume_name, + App.home_subvolume_name + }; + + var possible_layouts = new string[,]{ + {"", "", "Custom"}, + {"@", "@home", "Ubuntu (@, @home)"}, + {"@rootfs", "", "Debian (@rootfs)"}, + {"root", "home", "Fedora (root, home)"} + }; + + Gtk.ListStore list_store = new Gtk.ListStore (3, + typeof (string), + typeof (string), + typeof (string)); + Gtk.TreeIter store_iter; + int active = -1; + for (int idx = 0; idx < possible_layouts.length[0]; idx++) { + list_store.append(out store_iter); + list_store.set(store_iter, + 0, possible_layouts[idx, 0], + 1, possible_layouts[idx, 1], + 2, possible_layouts[idx, 2]); + + // Find our layout in the options + if (possible_layouts[idx, 0] == layout[0] && + possible_layouts[idx, 1] == layout[1]) active = idx; + } + + if (active < 0){ + active = 0; + } + + combo_subvol_layout = new Gtk.ComboBox.with_model (list_store); + hbox_subvolume.add (combo_subvol_layout); + sg_edit.add_widget(combo_subvol_layout); + + Gtk.CellRendererText renderer = new Gtk.CellRendererText (); + combo_subvol_layout.pack_start (renderer, true); + combo_subvol_layout.add_attribute (renderer, "text", 2); + + // Set active index + combo_subvol_layout.active = active; + + // Create custom inputs + vbox_subvolume_custom = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); + vbox.add(vbox_subvolume_custom); + + var custom_root_subvol_entry = add_opt_btrfs_subvolume_name_entry(vbox_subvolume_custom, sg_label, sg_edit, + "Root", App.root_subvolume_name); + + var custom_home_subvol_entry = add_opt_btrfs_subvolume_name_entry(vbox_subvolume_custom, sg_label, sg_edit, + "Home", App.home_subvolume_name); + + combo_subvol_layout.changed.connect (() => { + Gtk.TreeIter iter; + combo_subvol_layout.get_active_iter (out iter); + + // Handle custom names + if (combo_subvol_layout.active == 0) { + custom_root_subvol_entry.text = App.root_subvolume_name; + custom_home_subvol_entry.text = App.home_subvolume_name; + } + // Handle selection from combobox + else { + Value val1; + list_store.get_value (iter, 0, out val1); + App.root_subvolume_name = (string) val1; + + Value val2; + list_store.get_value (iter, 1, out val2); + App.home_subvolume_name = (string) val2; + + // If home subvolume name is empty, do not backup home. + // Unfortunately, due to how the settings and wizard dialogs work + // (changing the settings immediately), this might opaquely change + // the setting for the user to not include home subvolume in backups. + if (App.home_subvolume_name == "") + App.include_btrfs_home_for_backup = false; + } + + init_backend(); + type_changed(); + update_custom_subvol_name_visibility(); + }); + + custom_root_subvol_entry.focus_out_event.connect((entry1, event1) => { + App.root_subvolume_name = custom_root_subvol_entry.text; + + init_backend(); + type_changed(); + + return false; + }); + + custom_home_subvol_entry.focus_out_event.connect((entry1, event1) => { + App.home_subvolume_name = custom_home_subvol_entry.text; + + // If home subolume name is empty, do not backup home. + if (App.home_subvolume_name == "") + App.include_btrfs_home_for_backup = false; + + init_backend(); + type_changed(); + + return false; + }); + + // Add custom subvolume names + } + + private Gtk.Entry add_opt_btrfs_subvolume_name_entry(Gtk.Box vbox, Gtk.SizeGroup sg_title, + Gtk.SizeGroup sg_edit, string name, string value) { + // root subvolume name layout + var hbox_subvolume_edit = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + vbox.add(hbox_subvolume_edit); + + var lbl_subvol_name = new Gtk.Label(_(@"$name subvolume:")); + lbl_subvol_name.xalign = (float) 0.0; + hbox_subvolume_edit.add(lbl_subvol_name); + sg_title.add_widget(lbl_subvol_name); + + var entry_subvol = new Gtk.Entry(); + entry_subvol.text = value; + hbox_subvolume_edit.add(entry_subvol); + sg_edit.add_widget(entry_subvol); + + return entry_subvol; + } + + public void update_custom_subvol_name_visibility() { + vbox_subvolume_custom.visible = (combo_subvol_layout.active == 0); + } + private bool check_for_btrfs_tools() { try { const string args[] = {"lsblk", "-o", "FSTYPE", null}; @@ -214,5 +373,6 @@ class SnapshotBackendBox : Gtk.Box{ opt_btrfs.active = App.btrfs_mode; type_changed(); update_description(); + update_custom_subvol_name_visibility(); } } diff --git a/src/Gtk/SnapshotListBox.vala b/src/Gtk/SnapshotListBox.vala index 1e851690..92036071 100644 --- a/src/Gtk/SnapshotListBox.vala +++ b/src/Gtk/SnapshotListBox.vala @@ -431,12 +431,12 @@ class SnapshotListBox : Gtk.Box{ int64 size = 0; - if (bak.subvolumes.has_key("@")){ - size += bak.subvolumes["@"].total_bytes; + if (bak.subvolumes.has_key(App.root_subvolume_name)){ + size += bak.subvolumes[App.root_subvolume_name].total_bytes; } - if (bak.subvolumes.has_key("@home")){ - size += bak.subvolumes["@home"].total_bytes; + if (bak.subvolumes.has_key(App.home_subvolume_name)){ + size += bak.subvolumes[App.home_subvolume_name].total_bytes; } ctxt.text = format_file_size(size); @@ -467,11 +467,11 @@ class SnapshotListBox : Gtk.Box{ int64 size = 0; - if (bak.subvolumes.has_key("@")){ - size += bak.subvolumes["@"].unshared_bytes; + if (bak.subvolumes.has_key(App.root_subvolume_name)){ + size += bak.subvolumes[App.root_subvolume_name].unshared_bytes; } - if (bak.subvolumes.has_key("@home")){ - size += bak.subvolumes["@home"].unshared_bytes; + if (bak.subvolumes.has_key(App.home_subvolume_name)){ + size += bak.subvolumes[App.home_subvolume_name].unshared_bytes; } ctxt.text = format_file_size(size); diff --git a/src/Gtk/UsersBox.vala b/src/Gtk/UsersBox.vala index be549c07..177d3edf 100644 --- a/src/Gtk/UsersBox.vala +++ b/src/Gtk/UsersBox.vala @@ -322,7 +322,7 @@ class UsersBox : Gtk.Box{ if (restore_mode){ - chk_include_btrfs_home = new Gtk.CheckButton.with_label(_("Restore @home subvolume")); + chk_include_btrfs_home = new Gtk.CheckButton.with_label(_("Restore home subvolume")); box.add(chk_include_btrfs_home); @@ -333,7 +333,7 @@ class UsersBox : Gtk.Box{ } else { - chk_include_btrfs_home = new Gtk.CheckButton.with_label(_("Include @home subvolume in backups")); + chk_include_btrfs_home = new Gtk.CheckButton.with_label(_("Include home subvolume in backups")); box.add(chk_include_btrfs_home); @@ -357,6 +357,8 @@ class UsersBox : Gtk.Box{ box_btrfs.set_no_show_all(false); box_btrfs.show_all(); + + chk_include_btrfs_home.sensitive = (App.home_subvolume_name != ""); if (restore_mode){ chk_include_btrfs_home.active = App.include_btrfs_home_for_restore;