SnapshotRepo

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 repository
  • true – 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 successfully
  • invalid_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 status
  • status_details – additional diagnostic or corrective information
  • status_code – structured status enumeration
  • last_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:

  1. Store the supplied path and parent window
  2. Set the repository mode
  3. Initialize snapshot collections and the mount-path map
  4. Identify the backing filesystem with Device.get_disk_space_using_df()
  5. Create a synthetic device when no ordinary block-device record is available
  6. 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:

  1. Unlocks and mounts the required device or devices
  2. Loads the available snapshots
  3. Logs the selected device and free space
  4. 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
  • @home initially uses the same mount path
  • device_home initially 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:

  1. Check whether the device is encrypted
  2. Unlock it through unlock_encrypted_device()
  3. Select BTRFS mount options when required
  4. 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:

  1. Whether the repository device is available
  2. Whether the selected BTRFS device contains a valid system layout
  3. Whether snapshots exist
  4. 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 needed value 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:

  1. remove_untagged()
  2. remove_marked_for_deletion()
  3. 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:

  1. Deletes every loaded valid snapshot
  2. Removes the RSYNC synchronization directory
  3. Deletes the complete repository root
  4. 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 configured
  • NOT_AVAILABLE = -1 – the configured device cannot be found
  • HAS_SNAPSHOTS_HAS_SPACE = 0 – snapshots exist and free space is sufficient
  • HAS_SNAPSHOTS_NO_SPACE = 1 – snapshots exist but free space is insufficient
  • NO_SNAPSHOTS_NO_SPACE = 2 – no snapshots exist and the first snapshot cannot fit
  • NO_SNAPSHOTS_HAS_SPACE = 3 – no snapshots exist but sufficient space is available
  • READ_ONLY_FS = 4 – repository filesystem is read-only
  • HARDLINKS_NOT_SUPPORTED = 5 – target filesystem does not support required hard links
  • NO_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:

  1. Create the repository from a path, device, UUID, or empty state
  2. Resolve or synthesize the repository device
  3. Unlock encrypted storage when required
  4. Mount the root filesystem and optional separate home filesystem
  5. Select the BastionGuard or legacy Timeshift repository path
  6. Enumerate valid and invalid snapshots
  7. Normalize live-snapshot state
  8. Query BTRFS subvolume information when applicable
  9. Validate device availability and free space
  10. Apply automatic tag retention
  11. Remove untagged, marked, or incomplete snapshots
  12. 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=0 is used to access and manage all repository subvolumes.
  • Separate home support: the root and @home subvolumes 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 an smb: prefix.
  • df parsing: synthetic-device creation parses command output by spaces and assumes the standard df column layout.
  • Available-byte field: the synthetic device initially populates available_bytes, while later repository checks use free_bytes after a disk-space refresh.
  • Path constructor initialization: from_path() does not populate the @ and @home mount-map keys before status checking.
  • Encrypted secondary device: unlocking a device inside unlock_and_mount_device() assigns the unlocked device to the primary device field, 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].name instead of the name of the current snap being processed.
  • First-snapshot estimate: an uninitialized zero value in Main.first_snapshot_size causes 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-aware timeshift_path property.
  • Invalid snapshot deletion: incomplete snapshot objects are passed through the same engine-specific removal methods as valid snapshots.
  • Status definitions: READ_ONLY_FS and HARDLINKS_NOT_SUPPORTED are 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.