1. Overview
The SnapshotRepo class represents and manages the storage repository used by BastionGuard Backup.
It acts as the coordination layer between snapshot metadata, storage devices, mounted filesystems, BTRFS subvolumes, RSYNC repositories, retention policies, free-space validation, encrypted-device unlocking, snapshot discovery, deletion, and compatibility with legacy Timeshift repository layouts.
The class supports two repository modes:
- RSYNC mode – snapshots are stored as filesystem trees inside a dedicated repository directory
- BTRFS mode – snapshots are stored as native BTRFS subvolumes, potentially across separate root and home filesystems
The repository integrates the following internal components:
- Device – block-device discovery, disk-space queries, mounting, and LUKS unlocking
- Snapshot – snapshot metadata loading, tags, deletion, and control files
- Subvolume – BTRFS subvolume discovery and device association
- Main – global configuration, retention limits, system information, and mount paths
- Gio.File – filesystem enumeration and symbolic-link creation
- Gee.ArrayList – valid and invalid snapshot collections
- Gee.HashMap – per-subvolume mount paths
- TeeJee.FileSystem – path handling and recursive directory removal
- TeeJee.ProcessHelper – external command execution
- TeeJee.Logging – runtime, status, and error logging
2. Class Definition
The class derives from GLib.Object:
public class SnapshotRepo : GLib.Object
A repository object contains the selected storage device, mount locations, discovered snapshots, status information, and the operational mode used to interpret the repository layout.
3. Repository Device State
3.1 Primary Device
public Device device = null;
The device property represents the main storage device associated with the snapshot repository.
Depending on the initialization method, it may represent:
- A local block-device partition
- An unlocked child of a LUKS-encrypted partition
- A BTRFS system filesystem
- A synthetic remote-filesystem device
- A placeholder containing only a configured UUID
3.2 BTRFS Home Device
public Device device_home = null;
The device_home
It identifies the filesystem that contains the @home subvolume. By default, it references the same device as the root @ subvolume.
If @home resides on another filesystem, the repository discovers and mounts that device separately.
4. Mount Paths
4.1 Primary Mount Path
public string mount_path = "";
The mount_path property contains the mounted location of the primary repository device.
Typical runtime locations include:
/run/bastionguard-backup/<pid>/backup
/run/bastionguard-backup/<pid>/backup-home
4.2 Per-Subvolume Mount Map
public Gee.HashMap<string, string> mount_paths;
The map associates BTRFS subvolume names with the filesystem mount paths that contain them.
The principal keys are:
@
@home
In a single-device BTRFS layout, both keys point to the same path.
When @home is stored on another filesystem, the map contains two different mount locations.
5. Repository Mode
public bool btrfs_mode = false;
The btrfs_mode
false– RSYNC repositorytrue– BTRFS repository
6. Snapshot Collections
The repository maintains separate collections for valid and invalid snapshots:
public Gee.ArrayList<Snapshot?> snapshots;
public Gee.ArrayList<Snapshot?> invalid_snapshots;
snapshots– snapshots whose required metadata and files were loaded successfullyinvalid_snapshots– incomplete or malformed snapshot directories
Invalid snapshots can later be removed automatically by the repository cleanup process.
7. Repository Status
The current repository condition is represented by:
public string status_message = "";
public string status_details = "";
public SnapshotLocationStatus status_code;
public bool last_snapshot_failed_space = false;
status_message– concise user-facing statusstatus_details– additional diagnostic or corrective informationstatus_code– structured status enumerationlast_snapshot_failed_space– preserves a recent insufficient-space condition during status recalculation
8. Internal Thread State
Recursive repository-directory deletion uses the following internal fields:
private bool thr_success = false;
private bool thr_running = false;
private string thr_args1 = "";
A parent GTK window is also retained for encrypted-device dialogs:
private Gtk.Window? parent_window = null;
9. Repository Construction from a Path
9.1 SnapshotRepo.from_path()
public SnapshotRepo.from_path(
string path,
Gtk.Window? parent_win,
bool _btrfs_mode
)
This constructor initializes a repository from an already accessible filesystem path.
The initialization flow is:
- Store the supplied path and parent window
- Set the repository mode
- Initialize snapshot collections and the mount-path map
- Identify the backing filesystem with
Device.get_disk_space_using_df() - Create a synthetic device when no ordinary block-device record is available
- Evaluate the repository status
This constructor is particularly relevant for repositories accessed through mounted remote filesystems.
10. Synthetic Remote Devices
10.1 build_synthetic_device_for_path()
private static Device? build_synthetic_device_for_path(string path)
Some filesystems do not expose a normal block-device UUID. Examples include:
- CIFS and SMB shares
- NFS mounts
- SSHFS filesystems
- Other userspace or network-mounted filesystems
To support these repositories, the method executes:
df -T -B1 "<path>"
The command provides:
- Filesystem identifier
- Filesystem type
- Total size in bytes
- Used bytes
- Available bytes
- Usage percentage
The final non-empty output row is parsed into a new Device object.
10.2 Synthetic UUID
A non-empty synthetic UUID is generated using:
smb:<repository-path>
The device type is set to:
remote
This permits downstream code that expects a non-null device and non-empty UUID to operate uniformly for remote repositories.
11. Repository Construction from a Device
11.1 SnapshotRepo.from_device()
public SnapshotRepo.from_device(
Device dev,
Gtk.Window? parent_win,
bool btrfs_repo
)
This constructor initializes a repository from an already resolved Device object.
After initializing the collections, it delegates device processing to:
init_from_device();
12. Repository Construction from a UUID
12.1 SnapshotRepo.from_uuid()
public SnapshotRepo.from_uuid(
string uuid,
Gtk.Window? parent_win,
bool btrfs_repo
)
The constructor attempts to locate a device through:
Device.get_device_by_uuid(uuid)
If the device is not currently available, a placeholder Device is created containing the requested UUID.
This allows the repository to preserve configured device identity and report that the device is unavailable instead of treating it as unconfigured.
13. Empty Repository Construction
13.1 SnapshotRepo.from_null()
public SnapshotRepo.from_null()
This constructor creates a repository without a selected device.
It initializes the snapshot collections and mount map but performs no mounting, loading, or status validation.
This state is used when the user has not yet configured a snapshot location.
14. Device-Based Initialization
14.1 init_from_device()
The method verifies that:
- The device object exists
- The configured UUID is not empty
- The UUID currently resolves to an available device
If these conditions are satisfied, the repository:
- Unlocks and mounts the required device or devices
- Loads the available snapshots
- Logs the selected device and free space
- Checks the repository status
15. Repository Root Path
15.1 timeshift_path
public string timeshift_path
Despite retaining the legacy property name, this computed path supports both the BastionGuard and original Timeshift directory layouts.
15.2 BTRFS Repository Paths
In BTRFS mode, the preferred path is:
<mount-path>/bastionguard-backup-btrfs
If it does not exist, the class checks the legacy path:
<mount-path>/timeshift-btrfs
If neither directory exists, the new BastionGuard path is returned for future creation.
15.3 RSYNC Repository Paths
In RSYNC mode, the preferred path is:
<mount-path>/bastionguard-backup
The legacy fallback is:
<mount-path>/timeshift
16. Snapshot Directory Path
16.1 snapshots_path
public string snapshots_path
The property returns:
<repository-root>/snapshots
All snapshot discovery and repository symlink operations are based on this path.
17. Mounting Repository Devices
17.1 unlock_and_mount_devices()
This method mounts the primary repository device at:
<application-runtime-path>/backup
If mounting fails, the method returns false.
For RSYNC repositories, the @ and @home mount-map values are initialized as empty strings because native BTRFS subvolume paths are not required.
17.2 BTRFS Root and Home Mounting
In BTRFS mode:
@is associated with the primary mount path@homeinitially uses the same mount pathdevice_homeinitially references the primary device
The repository then detects system subvolumes below:
<mount-path>/@
If the discovered @home subvolume references another filesystem UUID, that device is mounted separately at:
<application-runtime-path>/backup-home
18. Mounting a Single Device
18.1 unlock_and_mount_device()
public string unlock_and_mount_device(
Device device_to_mount,
string path_to_mount
)
The method returns the mount path on success or an empty string on failure.
Its processing flow is:
- Check whether the device is encrypted
- Unlock it through
unlock_encrypted_device() - Select BTRFS mount options when required
- Mount the device through
Device.mount()
BTRFS repositories are mounted with:
subvolid=0
This exposes the top-level BTRFS filesystem and allows direct access to all subvolumes.
19. Encrypted Device Unlocking
19.1 unlock_encrypted_device()
public Device? unlock_encrypted_device(Device luks_device)
The method delegates LUKS unlocking to:
Device.luks_unlock()
The configured GTK parent window is passed so password or authentication dialogs can remain associated with the main application.
The method returns the unlocked mapped device or null when unlocking fails.
20. Snapshot Discovery
20.1 load_snapshots()
The method clears the existing valid and invalid collections before scanning:
<repository-root>/snapshots
Discovery stops immediately when:
- No repository device is available
- The snapshot directory does not exist
20.2 Directory Enumeration
Every direct child directory is treated as a potential snapshot, except:
.sync
Each directory is loaded through:
new Snapshot(
snapshot_path,
btrfs_mode,
this
)
Valid objects are added to snapshots, while incomplete objects are added to invalid_snapshots.
20.3 Snapshot Ordering
Valid snapshots are sorted by creation date in ascending order:
oldest → newest
This ordering is used by retention, oldest-snapshot selection, and repository cleanup logic.
21. Live BTRFS Snapshot Validation
After loading, the repository reviews snapshots whose live flag is enabled.
The flag is cleared when:
- The snapshot belongs to another system UUID
- The active system root cannot be identified
- The snapshot was created before the current system boot
When the flag changes, the snapshot control file is updated immediately.
A live snapshot remains active only when it belongs to the current system and was created after the most recent boot.
22. BTRFS Size Information
After loading a BTRFS repository, the class invokes:
App.query_subvolume_info(this);
This updates snapshot subvolume identifiers and space-consumption information through BTRFS quota-group queries.
23. Filtering Snapshots by Tag
23.1 get_snapshots_by_tag()
public Gee.ArrayList<Snapshot?> get_snapshots_by_tag(
string tag = ""
)
The method returns snapshots that:
- Match the supplied tag
- Or match all tags when the argument is empty
The returned collection is sorted chronologically from oldest to newest.
Common tags include:
boot
hourly
daily
weekly
monthly
ondemand
24. Latest and Oldest Snapshot Selection
24.1 get_latest_snapshot()
public Snapshot? get_latest_snapshot(
string tag,
string sys_uuid
)
The filtered list is scanned in reverse order. The first snapshot whose source-system UUID matches sys_uuid is returned.
24.2 get_oldest_snapshot()
public Snapshot? get_oldest_snapshot(
string tag,
string sys_uuid
)
The filtered list is scanned from the beginning. The first matching source-system UUID is returned.
Both methods return null when no suitable snapshot exists.
25. Repository Status Evaluation
25.1 check_status()
The method calculates the current repository state by checking:
- Whether the repository device is available
- Whether the selected BTRFS device contains a valid system layout
- Whether snapshots exist
- Whether sufficient free space is available
When the previous snapshot operation failed because of insufficient space, the existing low-space status is temporarily preserved instead of being overwritten by the normal initial state.
In GUI mode, the resulting device, path, message, details, and status code are written to the debug log.
26. Device Availability
26.1 available()
If no device object is available and no backup UUID is configured, the status becomes:
Snapshot device not selected
The associated status code is:
NOT_SELECTED
If a backup UUID is configured but its device cannot be found, the status becomes:
Snapshot device not available
The associated status code is:
NOT_AVAILABLE
In BTRFS mode, availability additionally requires a valid BTRFS system filesystem.
27. BTRFS System Validation
27.1 has_btrfs_system()
The expected root subvolume path is:
<mount-path-for-@>/@
If this directory is missing in BTRFS mode, the selected device is rejected with:
Selected snapshot device is not a system disk
The corresponding status code is:
NO_BTRFS_SYSTEM
28. Snapshot Presence
28.1 has_snapshots()
public bool has_snapshots()
The method returns:
snapshots.size > 0
It does not reload the repository automatically.
29. Free-Space Validation
29.1 has_space()
public bool has_space(uint64 needed = 0)
The repository first refreshes device-space information through:
device.query_disk_space();
The validation behavior differs depending on whether snapshots already exist.
29.2 Existing Repository
When snapshots exist, the required free space is:
- The explicit
neededvalue when greater than zero - Otherwise
Main.MIN_FREE_SPACE
If free space is below the threshold, the status becomes:
HAS_SNAPSHOTS_NO_SPACE
The last_snapshot_failed_space flag is also enabled.
When sufficient space remains, the status becomes:
HAS_SNAPSHOTS_HAS_SPACE
The details include the number of snapshots and formatted free space.
29.3 Empty Repository
When no snapshots exist, the required space is based on:
Main.first_snapshot_size
Insufficient space produces:
NO_SNAPSHOTS_NO_SPACE
Sufficient space produces:
NO_SNAPSHOTS_HAS_SPACE
The status details report the estimated size of the first snapshot.
30. Console Status Output
30.1 print_status()
The method recalculates the status and prints a formatted repository summary.
Displayed fields include:
- Device name
- Device UUID
- Repository mount path
- Snapshot mode
- Status message
- Status details
31. Automatic Retention
31.1 auto_remove()
The automatic cleanup routine applies configured retention limits to snapshot tags.
It processes:
- Boot snapshots
- Hourly snapshots
- Daily snapshots
- Weekly snapshots
- Monthly snapshots
The corresponding limits are read from:
App.count_boot
App.count_hourly
App.count_daily
App.count_weekly
App.count_monthly
31.2 Boot Retention
If the number of boot-tagged snapshots exceeds App.count_boot, the boot tag is removed from the oldest snapshots until the limit is satisfied.
31.3 Time-Based Retention
For hourly, daily, weekly, and monthly snapshots, the repository calculates both:
- A maximum count
- A corresponding age limit
Tags are removed only when:
- The snapshot is older than the calculated limit
- The number of snapshots still exceeds the configured count
- The snapshot does not contain a user description
Snapshots with comments are protected from this part of the automatic retention process.
31.4 Cleanup Phases
After adjusting tags, the routine executes:
remove_untagged()remove_marked_for_deletion()remove_invalid()
A snapshot is physically removed only after it no longer contains any tag, is explicitly marked, or is incomplete.
32. Removing Untagged Snapshots
32.1 remove_untagged()
Every valid snapshot with an empty tag list is deleted through:
snapshot.remove(true);
The repository is reloaded after the deletion pass.
33. Removing Marked Snapshots
33.1 remove_marked_for_deletion()
Snapshots whose persistent deletion marker is active are removed synchronously.
Afterward, the repository snapshot collections are reloaded.
34. Removing Invalid Snapshots
34.1 remove_invalid()
Every snapshot stored in invalid_snapshots is treated as incomplete and removed.
The repository is reloaded after cleanup.
35. Removing the Entire Repository
35.1 remove_all()
If the repository root exists, the method:
- Deletes every loaded valid snapshot
- Removes the RSYNC synchronization directory
- Deletes the complete repository root
- Reloads the snapshot collections
If no repository exists, the method reports that no snapshots were found and returns success.
36. Synchronization Directory Removal
36.1 remove_sync_dir()
The RSYNC synchronization directory is:
<mount-path>/bastionguard-backup/snapshots/.sync
If present, it is deleted recursively.
37. Repository Root Removal
37.1 remove_timeshift_dir()
The method removes the complete path returned by:
timeshift_path
This includes either a current BastionGuard repository or a detected legacy Timeshift repository.
38. Threaded Directory Deletion
38.1 delete_directory()
Recursive directory removal runs in a dedicated thread named:
delete-directory
The caller waits while:
thr_running == true
During the wait loop, GTK events are processed approximately every 100 milliseconds to preserve interface responsiveness.
38.2 delete_directory_thread()
The worker delegates removal to:
TeeJee.FileSystem.dir_delete_recursive()
The result is stored in thr_success, and the operation is logged as either successful or failed.
39. Snapshot Symlink Structure
39.1 create_symlinks()
The repository maintains tag-based symbolic-link directories for:
boot
hourly
daily
weekly
monthly
ondemand
Before links are created, every tag directory is cleared and recreated.
For each snapshot tag, the link path is:
<snapshots-path>-<tag>/<snapshot-name>
The symbolic-link target is relative:
../snapshots/<snapshot-name>
Example:
bastionguard-backup/
├── snapshots/
│ └── 2026-07-12_10-30-00/
├── snapshots-daily/
│ └── 2026-07-12_10-30-00 -> ../snapshots/2026-07-12_10-30-00
└── snapshots-weekly/
└── 2026-07-12_10-30-00 -> ../snapshots/2026-07-12_10-30-00
40. Symlink Directory Cleanup
40.1 cleanup_symlink_dir()
The tag directory path is generated as:
<snapshots-path>-<tag>
The existing directory and all of its links are removed recursively. A new empty directory is then created with all required parent directories.
41. Repository Status Enumeration
public enum SnapshotLocationStatus
The enumeration defines the structured repository states used by the application.
NOT_SELECTED = -2– no snapshot device has been configuredNOT_AVAILABLE = -1– the configured device cannot be foundHAS_SNAPSHOTS_HAS_SPACE = 0– snapshots exist and free space is sufficientHAS_SNAPSHOTS_NO_SPACE = 1– snapshots exist but free space is insufficientNO_SNAPSHOTS_NO_SPACE = 2– no snapshots exist and the first snapshot cannot fitNO_SNAPSHOTS_HAS_SPACE = 3– no snapshots exist but sufficient space is availableREAD_ONLY_FS = 4– repository filesystem is read-onlyHARDLINKS_NOT_SUPPORTED = 5– target filesystem does not support required hard linksNO_BTRFS_SYSTEM = 6– selected BTRFS device does not contain an@root subvolume
The read-only and hard-link status values are defined for repository validation but are not assigned by methods shown in this class.
42. Repository Lifecycle
The complete repository lifecycle is:
- Create the repository from a path, device, UUID, or empty state
- Resolve or synthesize the repository device
- Unlock encrypted storage when required
- Mount the root filesystem and optional separate home filesystem
- Select the BastionGuard or legacy Timeshift repository path
- Enumerate valid and invalid snapshots
- Normalize live-snapshot state
- Query BTRFS subvolume information when applicable
- Validate device availability and free space
- Apply automatic tag retention
- Remove untagged, marked, or incomplete snapshots
- Rebuild tag-based symbolic-link directories
43. Runtime and Security Considerations
- Encrypted storage: LUKS repositories are unlocked through the standard device abstraction.
- Mount isolation: devices are mounted below a process-specific BastionGuard runtime path.
- BTRFS top-level access:
subvolid=0is used to access and manage all repository subvolumes. - Separate home support: the root and
@homesubvolumes may reside on different devices. - Legacy compatibility: existing Timeshift RSYNC and BTRFS repositories remain discoverable.
- Remote filesystem support: synthetic devices permit mounted network filesystems to participate in repository logic.
- Free-space enforcement: repository status distinguishes existing-snapshot requirements from first-snapshot estimates.
- Metadata validation: incomplete snapshots are isolated from the valid snapshot collection.
- Retention safety: comments protect scheduled snapshots from most automatic tag removal.
- Persistent deletion: snapshots marked for deletion are removed during automatic cleanup.
- UI responsiveness: recursive repository deletion runs in a worker thread while GTK events continue to be processed.
- Relative symlinks: tag indexes remain valid when the repository is mounted at a different absolute location.
44. Implementation Considerations
- Remote-device identifier: the source comment refers to a
sha:prefix, while the implementation generates ansmb:prefix. - df parsing: synthetic-device creation parses command output by spaces and assumes the standard
dfcolumn layout. - Available-byte field: the synthetic device initially populates
available_bytes, while later repository checks usefree_bytesafter a disk-space refresh. - Path constructor initialization:
from_path()does not populate the@and@homemount-map keys before status checking. - Encrypted secondary device: unlocking a device inside
unlock_and_mount_device()assigns the unlocked device to the primarydevicefield, including when processing a separate home device. - Tag filter precedence: the condition inside
get_snapshots_by_tag()combines&&and||without explicit outer grouping. - Boot comment handling: boot-tag retention does not check whether a snapshot contains a description before removing its tag.
- Retention logging: scheduled-tag removal logs
list[0].nameinstead of the name of the currentsnapbeing processed. - First-snapshot estimate: an uninitialized zero value in
Main.first_snapshot_sizecauses an empty repository to pass the initial space test. - Shared thread fields: threaded directory deletion uses repository-level mutable fields and is not designed for concurrent deletion requests.
- Hardcoded synchronization path:
remove_sync_dir()targets the new RSYNC directory name directly and does not use the legacy-awaretimeshift_pathproperty. - Invalid snapshot deletion: incomplete snapshot objects are passed through the same engine-specific removal methods as valid snapshots.
- Status definitions:
READ_ONLY_FSandHARDLINKS_NOT_SUPPORTEDare defined but not produced within this class. - Immediate metadata updates: resetting stale live flags rewrites each affected snapshot control file during repository loading.
- Repository-wide deletion:
remove_all()deletes the repository root after processing only the currently loaded valid snapshots.
45. Licensing
The source file is distributed under the GNU General Public License, version 2 or, at the user’s option, any later version.
The original copyright notice identifies:
Copyright 2012-2018 Tony George
<teejeetech@gmail.com>
The software is provided without warranty, including implied warranties of merchantability or fitness for a particular purpose.
