Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d528da
Added ui for btrfs subvolume names
fused0 Nov 23, 2025
85efc0b
Merge branch 'linuxmint:master' into btrfs_subvol_names
fused0 Nov 23, 2025
6cf8745
Improve subvolume name option layout
fused0 Nov 23, 2025
bd54ed4
early out of subvolume listing if configured subvolume is not the sam…
fused0 Nov 23, 2025
f3e3bc8
fix add missing return value in Main.query_subvolume_id
fused0 Nov 23, 2025
52b7cf4
Subvolume selection is now a combobox
fused0 Nov 24, 2025
9cc883b
improved subvolume selection by adding a distribution hint
fused0 Nov 24, 2025
0ebe92a
combine subvol comboboxes
fused0 Nov 24, 2025
4e13156
detect subvolume layout
fused0 Nov 24, 2025
69b8468
Merge branch 'linuxmint:master' into btrfs_subvol_names
fused0 Nov 24, 2025
04bfffd
cleanup create_btrfs_subvolume_selection
fused0 Nov 24, 2025
dd769f5
call init_backend on layout change
fused0 Nov 24, 2025
d7b5070
also call type_changed on subvol layout change
fused0 Nov 24, 2025
0e71046
Disable "include home subvol" option when home subvolume name is empty
fused0 Nov 30, 2025
7f55d83
fix for debian style layout
fused0 Nov 30, 2025
aaccff3
added ui for custom subvolume layout
fused0 Nov 30, 2025
a1f3892
Merge branch 'linuxmint:master' into btrfs_subvol_names
fused0 Nov 30, 2025
44edb09
Change variable name in SnapshotBackendBox
fused0 Dec 8, 2025
7528ba9
Simplify toggling checkbox sensitivity in UsersBox
fused0 Dec 8, 2025
deb0ecb
Change early out of query_subvolume_id to has_key
fused0 Dec 8, 2025
6a0240f
Properly fix query_subvolume_id
fused0 Dec 8, 2025
216393b
Simplify vbox_subvolume_custom.visible assignment
fused0 Dec 8, 2025
59339bb
Fix typo in comment
fused0 Dec 8, 2025
b32ac4e
Making sure to consistently call Main::check_btrfs_layout_system befo…
fused0 Dec 9, 2025
4b5cf13
fix null pointer exception in query_subvolume_quota
fused0 Dec 9, 2025
01f0445
Merge branch 'linuxmint:master' into btrfs_subvol_names
fused0 Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 72 additions & 42 deletions src/Core/Main.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 ------------------------

Expand Down Expand Up @@ -488,9 +508,9 @@ public class Main : GLib.Object{

log_debug("check_btrfs_layout_system()");

bool supported = sys_subvolumes.has_key("@");
bool supported = sys_subvolumes.has_key(root_subvolume_name);
if (include_btrfs_home_for_backup){
supported = supported && sys_subvolumes.has_key("@home");
supported = supported && sys_subvolumes.has_key(home_subvolume_name);
}

if (!supported){
Expand Down Expand Up @@ -521,18 +541,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);
}
else{
supported = supported && check_btrfs_volume(dev_root, "@", unlock);
supported = supported && check_btrfs_volume(dev_root, root_subvolume_name, unlock);
}
}
}
Expand Down Expand Up @@ -1720,9 +1740,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 ---------------------------------
Expand All @@ -1739,11 +1759,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){
Expand Down Expand Up @@ -1786,7 +1806,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;

Expand Down Expand Up @@ -2344,11 +2364,11 @@ public class Main : GLib.Object{
// final check - check if target root device is mounted

if (btrfs_mode){
if (repo.mount_paths["@"].length == 0){
if (repo.mount_paths[root_subvolume_name].length == 0){
log_error(_("BTRFS device is not mounted") + ": @");
return false;
}
if (include_btrfs_home_for_restore && (repo.mount_paths["@home"].length == 0)){
if (include_btrfs_home_for_restore && (repo.mount_paths[home_subvolume_name].length == 0)){
log_error(_("BTRFS device is not mounted") + ": @home");
return false;
}
Expand Down Expand Up @@ -2430,7 +2450,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;
Expand Down Expand Up @@ -2466,7 +2486,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;
Expand Down Expand Up @@ -3166,7 +3186,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();
}
Expand Down Expand Up @@ -3230,12 +3250,12 @@ public class Main : GLib.Object{

if (found){
//delete system subvolumes
if (sys_subvolumes.has_key("@") && snapshot_to_restore.subvolumes.has_key("@")){
sys_subvolumes["@"].remove();
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") + ": @");
}
if (include_btrfs_home_for_restore && sys_subvolumes.has_key("@home") && snapshot_to_restore.subvolumes.has_key("@home")){
sys_subvolumes["@home"].remove();
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");
}

Expand Down Expand Up @@ -3263,9 +3283,9 @@ public class Main : GLib.Object{

var subvol_list = new Gee.ArrayList<Subvolume>();

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){
Expand Down Expand Up @@ -3294,7 +3314,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));
Expand All @@ -3308,11 +3328,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);
Expand Down Expand Up @@ -3357,6 +3377,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());
Expand Down Expand Up @@ -3476,6 +3499,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);
Expand Down Expand Up @@ -3544,7 +3570,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;
Expand Down Expand Up @@ -3959,8 +3985,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;
}
Expand All @@ -3983,7 +4009,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;
}
}
Expand Down Expand Up @@ -4109,16 +4135,20 @@ 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;
}

public bool query_subvolume_id(string subvol_name){

log_debug("query_subvolume_id():%s".printf(subvol_name));

// Early out when configured subvolume name != actual.
if(sys_subvolumes[root_subvolume_name] == null || sys_subvolumes[home_subvolume_name] == null)
return false;

string cmd = "";
string std_out;
Expand Down Expand Up @@ -4149,14 +4179,14 @@ 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["@"];
if ((sys_subvolumes.size > 0) && line.has_suffix(sys_subvolumes[root_subvolume_name].path.replace(repo.mount_paths[root_subvolume_name] + "/"," "))){
subvol = sys_subvolumes[root_subvolume_name];
}
else if ((sys_subvolumes.size > 0)
&& sys_subvolumes.has_key("@home")
&& line.has_suffix(sys_subvolumes["@home"].path.replace(repo.mount_paths["@home"] + "/"," "))){
&& 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];
}
else {
foreach(var bak in repo.snapshots){
Expand All @@ -4178,9 +4208,9 @@ 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;
}
Expand Down Expand Up @@ -4241,15 +4271,15 @@ public class Main : GLib.Object{

Subvolume subvol = null;

if ((sys_subvolumes.size > 0) && (sys_subvolumes["@"].id == subvol_id)){
if ((sys_subvolumes.size > 0) && (sys_subvolumes[root_subvolume_name].id == subvol_id)){

subvol = sys_subvolumes["@"];
subvol = sys_subvolumes[root_subvolume_name];
}
else if ((sys_subvolumes.size > 0)
&& sys_subvolumes.has_key("@home")
&& (sys_subvolumes["@home"].id == subvol_id)){
&& 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];
Comment on lines +4288 to +4291
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the issue on lines 4186-4189, this code is vulnerable to null pointer exceptions when home_subvolume_name is empty. If home_subvolume_name is an empty string (as it is for Debian), attempting to access sys_subvolumes[home_subvolume_name].id could fail.

Add an empty string check:

else if ((sys_subvolumes.size > 0)
    && (home_subvolume_name != "")
    && sys_subvolumes.has_key(home_subvolume_name)
    && (sys_subvolumes[home_subvolume_name].id == subvol_id)){

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this follows, as it checks if sys_subvolumes has the key home_subvolume_name - sys_subvolumes itself is generated from fstab and crypttab, so doesn't depend on home_subvolume_name or root_subvolume_name.

}
else {
foreach(var bak in repo.snapshots){
Expand Down
4 changes: 2 additions & 2 deletions src/Core/Snapshot.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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; }
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When App.home_subvolume_name is empty (e.g., for Debian), this condition will check subvol_name != "" which will always be true for non-empty names. This could cause the code to skip processing home subvolumes when it shouldn't, or process them when it shouldn't.

Consider handling empty home subvolume name explicitly:

if ((subvol_name != App.root_subvolume_name) && ((App.home_subvolume_name == "") || (subvol_name != App.home_subvolume_name))){ continue; }
Suggested change
if ((subvol_name != App.root_subvolume_name)&&(subvol_name != App.home_subvolume_name)){ continue; }
if ((subvol_name != App.root_subvolume_name) && ((App.home_subvolume_name == "") || (subvol_name != App.home_subvolume_name))) { continue; }

Copilot uses AI. Check for mistakes.

paths[subvol_name] = path.replace(repo.mount_path, repo.mount_paths[subvol_name]);

Expand Down
20 changes: 10 additions & 10 deletions src/Core/SnapshotRepo.vala
Original file line number Diff line number Diff line change
Expand Up @@ -193,31 +193,31 @@ public class SnapshotRepo : GLib.Object{
}

// rsync
mount_paths["@"] = "";
mount_paths["@home"] = "";
mount_paths[App.root_subvolume_name] = "";
mount_paths[App.home_subvolume_name] = "";

if (btrfs_mode){

mount_paths["@"] = mount_path;
mount_paths["@home"] = mount_path; //default
mount_paths[App.root_subvolume_name] = mount_path;
mount_paths[App.home_subvolume_name] = mount_path; //default
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 (repo_subvolumes.has_key(App.home_subvolume_name)){
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When App.home_subvolume_name is empty (e.g., for Debian), this will check has_key("") which could match unintended entries in the repo_subvolumes map.

Add an empty string check:

if ((App.home_subvolume_name != "") && repo_subvolumes.has_key(App.home_subvolume_name)){
Suggested change
if (repo_subvolumes.has_key(App.home_subvolume_name)){
if ((App.home_subvolume_name != "") && repo_subvolumes.has_key(App.home_subvolume_name)){

Copilot uses AI. Check for mistakes.

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;
}
}
Expand Down Expand Up @@ -505,7 +505,7 @@ public class SnapshotRepo : GLib.Object{

log_debug("SnapshotRepo: has_btrfs_system()");

var root_path = path_combine(mount_paths["@"],"@");
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("btrfs_mode=%s".printf(btrfs_mode.to_string()));
if (btrfs_mode){
Expand Down
Loading
Loading