1. Overview
The TimeshiftImport module provides detection, inspection, and migration utilities for snapshot repositories that are intended to be imported into BastionGuard Backup.
The implementation is contained in the BastionGuardImport namespace and is organized around three classes:
- ImportedSnapshot – lightweight representation of a discovered snapshot
- TimeshiftDetectResult – aggregate result of the detection process
- TimeshiftImporter – controller for configuration discovery, device mounting, snapshot scanning, and migration
The module performs the following operations:
- Searches for an existing backup configuration
- Reads the configured backup-device UUID and snapshot mode
- Resolves the corresponding block device
- Mounts the device temporarily in read-only mode
- Scans RSYNC or BTRFS snapshot directories
- Reads snapshot metadata from
info.json - Returns a structured inventory of discovered snapshots
- Optionally migrates metadata and application configuration
2. Namespace
All import-related classes are defined inside:
namespace BastionGuardImport
The namespace separates repository-import functionality from the normal snapshot repository, backup, and restore controllers.
3. Dependencies
The module imports:
using GLib;
using Json;
using TeeJee.Logging;
using TeeJee.FileSystem;
using TeeJee.JsonHelper;
The principal dependencies are:
- GLib.FileUtils – file tests, reading, writing, removal, and renaming
- GLib.Process – synchronous execution of system commands
- GLib.Dir – snapshot-directory enumeration
- Json.Parser – parsing configuration and snapshot metadata
- Json.Generator – rewriting imported JSON documents
- Gee.ArrayList – collection of discovered snapshots
- TeeJee.Logging – debug, operational, and error logging
- TeeJee.FileSystem – creation of configuration and temporary mount directories
- TeeJee.JsonHelper – typed access to JSON members
4. ImportedSnapshot Class
public class ImportedSnapshot : GLib.Object
The ImportedSnapshot class is a lightweight data-transfer object representing one snapshot discovered during repository scanning.
It does not inherit from the native Snapshot class and does not directly expose backup, restore, tagging, or deletion operations.
5. Imported Snapshot Identity
5.1 Snapshot Name
public string name = "";
The name field contains the snapshot directory name.
A typical timestamp-based name is:
2025-06-01_12-00-00
5.2 Snapshot Path
public string path = "";
The path field contains the complete snapshot path on the temporarily mounted backup device.
6. Imported Snapshot Date
public string date = "";
public int64 date_unix = 0;
date_unix– creation timestamp read frominfo.jsondate– local-time representation formatted for display
The formatted value uses:
%Y-%m-%d %H:%M:%S
7. Imported Snapshot Metadata
The source-system and descriptive fields are:
public string sys_uuid = "";
public string sys_distro = "";
public string tags = "";
public string description = "";
sys_uuid– source-system filesystem UUIDsys_distro– distribution description stored in the snapshottags– snapshot tag string copied from the control filedescription– user comments associated with the snapshot
The tag string is stored without normalization or conversion.
8. Snapshot Mode and Validity
public bool is_btrfs = false;
public bool valid = false;
is_btrfs– identifies the repository engine selected by the imported configurationvalid– becomestrueafter a readable and parseableinfo.jsonhas been loaded
9. TimeshiftDetectResult Class
public class TimeshiftDetectResult : GLib.Object
The TimeshiftDetectResult class contains the complete outcome of a detection operation.
It combines configuration information, device information, mount state, repository mode, snapshot paths, and the discovered snapshot inventory.
10. Detection-State Fields
public bool found = false;
public string config_path = "";
public string backup_uuid = "";
public string backup_device = "";
public string mount_path = "";
public bool is_btrfs = false;
public string snapshots_path = "";
found– indicates that a supported configuration file was foundconfig_path– path of the discovered JSON configurationbackup_uuid– configured backup-device UUIDbackup_device– resolved device path, such as/dev/sda2mount_path– temporary mount location used during scanningis_btrfs– configured snapshot-engine modesnapshots_path– complete path of the discovered snapshot directory
The found flag indicates configuration-file presence. It does not guarantee that the configuration was valid, the device was available, the mount succeeded, or snapshots were found.
11. Discovered Snapshot Collection
public Gee.ArrayList<ImportedSnapshot> snapshots;
The constructor initializes an empty collection:
snapshots = new Gee.ArrayList<ImportedSnapshot>();
Only snapshots whose metadata is considered valid are added to this list.
12. TimeshiftImporter Class
public class TimeshiftImporter : GLib.Object
The TimeshiftImporter class implements the main detection and migration workflow.
Its public interface consists of:
detect()– detects and inventories an existing repositorymigrate()– updates the detected repository and BastionGuard configuration
All device resolution, mounting, scanning, JSON parsing, and configuration updates are implemented by private helper methods.
13. Configuration Paths
The importer defines the following constants:
private const string TIMESHIFT_CONF =
"/etc/bastionguard-backup/bastionguard-backup.json";
private const string TIMESHIFT_CONF_LEGACY =
"/etc/bastionguard-backup.json";
private const string BGBACKUP_CONF =
"/etc/bastionguard-backup/bastionguard-backup.json";
private const string BGBACKUP_CONF_DIR =
"/etc/bastionguard-backup";
The canonical import path and BastionGuard destination configuration path currently reference the same file:
/etc/bastionguard-backup/bastionguard-backup.json
The fallback configuration path is:
/etc/bastionguard-backup.json
14. Temporary Mount Point
private const string SCAN_MOUNT_POINT =
"/run/bastionguard-backup/import-scan";
The backup device is mounted at a fixed temporary runtime path during detection and migration.
The directory is created before the mount operation begins.
15. Detection Entry Point
15.1 detect()
public TimeshiftDetectResult detect()
The detect() method is the principal repository-discovery entry point.
It always returns a TimeshiftDetectResult, even when configuration discovery, device resolution, mounting, or snapshot scanning fails.
16. Detection Workflow
The complete detection sequence is:
- Create an empty detection result
- Search for a supported configuration file
- Read the backup UUID and BTRFS mode
- Resolve the backup device from its UUID
- Mount the device in read-only mode
- Calculate the RSYNC or BTRFS snapshot path
- Scan snapshot directories
- Parse each snapshot’s
info.json - Sort valid snapshots from newest to oldest
- Return the populated result
17. Configuration Discovery
The method checks the canonical path first:
/etc/bastionguard-backup/bastionguard-backup.json
If it does not exist, it checks:
/etc/bastionguard-backup.json
If neither file exists, detection ends immediately with:
found = false
When a file exists, the method sets:
result.found = true;
result.config_path = conf_path;
These values are assigned before the file contents are validated.
18. Configuration Loading
18.1 load_timeshift_config()
private bool load_timeshift_config(
string path,
TimeshiftDetectResult result
)
The method reads the JSON configuration and extracts:
backup_device_uuid
btrfs_mode
The values are loaded through:
json_get_string()
json_get_bool()
The backup UUID is mandatory. An empty value causes configuration loading to fail.
18.2 Configuration Errors
The method returns false when:
- The file cannot be read
- The JSON cannot be parsed
backup_device_uuidis absent or empty
Each failure is written to the application log.
19. Device Resolution
19.1 find_device_by_uuid()
private string find_device_by_uuid(string uuid)
The method resolves a filesystem UUID into a device path.
An empty UUID immediately returns an empty result.
19.2 UUID Symbolic Link
The primary lookup path is:
/dev/disk/by-uuid/<UUID>
If the symbolic link exists, it is resolved with:
readlink -f /dev/disk/by-uuid/<UUID>
A successful command returns a canonical device path such as:
/dev/sda2
/dev/nvme0n1p3
19.3 blkid Fallback
If the symbolic-link lookup fails, the method executes:
blkid -U <UUID>
The trimmed command output is returned when the command succeeds.
If both lookup strategies fail, the method returns an empty string.
20. Read-Only Device Mounting
20.1 mount_backup_device()
private bool mount_backup_device(
TimeshiftDetectResult result
)
The method creates the temporary runtime directory and assigns it to:
result.mount_path
The device is mounted read-only using:
mount -o ro <device> /run/bastionguard-backup/import-scan
On command failure, the captured standard error is written to the log and the method returns false.
21. Repository Path Selection
For BTRFS mode, detection scans:
<mount-path>/bastionguard-backup-btrfs/snapshots
For RSYNC mode, detection scans:
<mount-path>/bastionguard-backup/snapshots
If the expected directory does not exist, the condition is logged and the result is returned with an empty snapshot collection.
22. Snapshot Directory Scanning
22.1 scan_snapshots()
private void scan_snapshots(
TimeshiftDetectResult result
)
The method opens the configured snapshot directory through:
Dir.open(result.snapshots_path)
Each direct child that is a directory is treated as a potential snapshot.
Non-directory entries are ignored.
22.2 Snapshot Parsing
Each candidate is processed through:
parse_snapshot(
snapshot_path,
directory_name,
result.is_btrfs
)
Only entries whose valid field becomes true are added to the result collection.
22.3 Snapshot Ordering
After scanning, snapshots are sorted by Unix creation time in descending order:
newest → oldest
The comparison is implemented through:
return (int)(b.date_unix - a.date_unix);
23. Snapshot Metadata Parsing
23.1 parse_snapshot()
private ImportedSnapshot parse_snapshot(
string path,
string name,
bool is_btrfs
)
The method initializes the snapshot’s:
- Name
- Filesystem path
- BTRFS mode
It then searches for:
<snapshot-path>/info.json
A missing control file leaves the snapshot invalid.
23.2 Loaded Snapshot Fields
The following JSON members are loaded:
sys-uuid
sys-distro
tags
comments
created
They are mapped to:
sys_uuidsys_distrotagsdescriptiondate_unix
23.3 Date Conversion
The created member is loaded as a string and parsed into an int64.
The value is interpreted as a UTC Unix timestamp:
new DateTime.from_unix_utc(snap.date_unix)
It is then converted to local time and formatted for display.
23.4 Snapshot Validity
The snapshot is marked valid only after all metadata parsing operations complete successfully:
snap.valid = true;
Malformed timestamps or JSON data leave the snapshot invalid and produce an error log.
24. Migration Entry Point
24.1 migrate()
public bool migrate(
TimeshiftDetectResult detected,
bool remove_timeshift_config = false,
Gtk.Window? parent_window = null
)
The method performs repository and configuration migration using a result previously returned by detect().
The optional parent_window parameter is accepted but is not used by the current implementation.
25. Migration Preconditions
Migration is rejected when:
detected.foundisfalse- No valid snapshots were discovered
The method does not migrate an empty repository.
26. Read-Write Remount
The device initially mounted read-only is remounted with:
mount -o remount,rw <device> <mount-path>
A non-zero exit status stops the migration.
The migration proceeds only after the filesystem has been remounted as writable.
27. Repository Directory Migration
The migration calculates source and destination repository paths according to the snapshot engine.
27.1 BTRFS Paths
old_base = <mount>/bastionguard-backup-btrfs
new_base = <mount>/bastionguard-backup-btrfs
27.2 RSYNC Paths
old_base = <mount>/bastionguard-backup
new_base = <mount>/bastionguard-backup
If the source path exists, the method attempts to rename it through:
FileUtils.rename(old_base, new_base)
After a successful rename, detected.snapshots_path is updated to the destination’s snapshots directory.
28. Snapshot Path Updates
For each discovered snapshot, its stored path is updated through:
snap.path = snap.path.replace(old_base, new_base);
The corresponding control file is then calculated as:
<updated-snapshot-path>/info.json
29. Snapshot Metadata Migration
Every readable info.json receives:
app-version
imported-from
The application version is replaced with:
bastionguard-backup-<Constants.VERSION>
The import source is recorded as:
imported-from = "bastionguard-backup"
The existing JSON object is otherwise preserved.
30. Metadata Serialization
The modified JSON document is serialized with pretty formatting:
gen.set_pretty(true);
It is written directly to the existing control-file path through:
FileUtils.set_contents(info_path, new_json);
A failure to update one snapshot is logged, but processing continues with the remaining snapshots.
31. BastionGuard Configuration Update
31.1 update_bgbackup_config()
The method creates the configuration directory when necessary:
/etc/bastionguard-backup
It then loads the existing configuration or creates a new empty JSON object.
31.2 Updated Configuration Members
The following values are written:
backup_device_uuid
btrfs_mode
imported_from_timeshift
The device UUID is copied from the detection result.
The BTRFS mode is serialized as a string:
"true"
"false"
The import marker is also serialized as a string:
"true"
31.3 Configuration Output
The resulting document is written to:
/etc/bastionguard-backup/bastionguard-backup.json
The JSON generator uses pretty formatting but does not explicitly configure indentation width.
32. Optional Source-Configuration Removal
When remove_timeshift_config is enabled, the migration attempts to remove:
/etc/bastionguard-backup/bastionguard-backup.json
/etc/bastionguard-backup.json
Successful removal is recorded in the application log.
Errors are logged without aborting the remaining migration cleanup.
33. Device Unmounting
33.1 unmount_backup_device()
private void unmount_backup_device(string mount_path)
The method executes:
umount <mount-path>
A non-zero status is logged as an unmount failure.
The migration process invokes this method after configuration and metadata updates.
34. Migration Return Value
The migration maintains an ok flag initialized to true.
The flag becomes false when repository-directory renaming fails.
Errors encountered while updating individual info.json files or writing the BastionGuard configuration are logged but do not modify the return value.
The method therefore reports the repository-renaming result rather than a complete transactional migration result.
35. Detection Data Flow
The complete detection data flow is:
- Locate the configuration file
- Set the configuration-presence flag
- Parse the backup UUID and mode
- Resolve the device through
/dev/disk/by-uuidorblkid - Create the fixed runtime mount point
- Mount the filesystem read-only
- Select the RSYNC or BTRFS repository path
- Enumerate snapshot directories
- Parse each
info.json - Discard invalid entries
- Sort valid entries from newest to oldest
- Return the detection result
36. Migration Data Flow
The complete migration flow is:
- Validate the previous detection result
- Remount the backup filesystem as read-write
- Calculate source and destination repository paths
- Rename the repository directory
- Update each imported snapshot path
- Rewrite each available
info.json - Update or create the BastionGuard configuration
- Optionally remove the source configuration
- Unmount the backup filesystem
- Return the migration status
37. Privilege Requirements
The importer performs operations that normally require root privileges:
- Reading protected configuration under
/etc - Mounting block devices
- Remounting filesystems as read-write
- Unmounting filesystems
- Writing BastionGuard configuration under
/etc - Renaming repository directories on the backup device
- Updating snapshot control files
The class does not invoke pkexec or sudo. It assumes that the containing application already has sufficient privileges.
38. Runtime and Security Considerations
- Read-only detection mount: initial scanning mounts the repository with
-o ro. - Explicit write transition: migration requires a successful read-write remount.
- UUID-based selection: the target storage device is selected from the configured filesystem UUID.
- Snapshot validation: only directories containing parseable
info.jsonmetadata are exposed. - Partial-failure tolerance: failure to update one snapshot does not stop metadata processing for other snapshots.
- Source traceability: migrated metadata receives an
imported-frommember. - Configuration preservation: existing destination configuration members are preserved when the document is successfully parsed.
- No encrypted-device handling: the module resolves ordinary filesystem UUIDs but does not unlock LUKS containers.
- No rollback: the migration does not restore prior files or directory names after a partial failure.
- Fixed mount point: all importer instances use the same runtime directory.
39. Implementation Considerations
- Naming mismatch: the class and method names reference Timeshift, while the configured paths and log messages reference BastionGuard Backup.
- Identical canonical paths:
TIMESHIFT_CONFandBGBACKUP_CONFcontain exactly the same path. - Identical repository paths: both the old and new RSYNC paths are
bastionguard-backup, while both BTRFS paths arebastionguard-backup-btrfs. - No-op rename: the migration currently attempts to rename a repository directory to the same path.
- Configuration removal conflict: enabling
remove_timeshift_configcan remove the same canonical configuration file thatupdate_bgbackup_config()has just written. - Detection mount lifetime:
detect()mounts the device but does not unmount it before returning. - Comment mismatch: the method documentation states that detection unmounts after scanning, but unmounting occurs only at the end of
migrate(). - Early found flag:
foundis set before JSON parsing, device discovery, mounting, and snapshot validation. - Fixed mount collision: concurrent importer operations can conflict over
/run/bastionguard-backup/import-scan. - Existing mount state: the mounting helper does not check whether the device or target directory is already mounted.
- BTRFS top-level access: the device is mounted without
subvolid=0; a filesystem with a non-top-level default subvolume may not expose the expected repository path. - Unquoted command arguments: UUID, device, and mount values are inserted directly into command strings.
- Configuration trust boundary: a modified UUID value can influence the shell command used by the
blkidfallback. - Nested exception handling: mount and remount operations contain redundant nested
try/catchblocks forGLib.SpawnError. - Status initialization: when command spawning raises an exception, subsequent use of the command status depends on compiler control-flow handling.
- Non-transactional writes: snapshot and application JSON files are rewritten directly instead of using temporary files and atomic renames.
- No metadata backup: existing
info.jsonfiles are not copied before modification. - Partial success reporting: configuration-write and per-snapshot metadata errors do not cause
migrate()to returnfalse. - Unused parent window: the
parent_windowparameter ofmigrate()is not referenced. - String-encoded Boolean values:
btrfs_modeandimported_from_timeshiftare written as JSON strings instead of native Boolean members. - Comparator narrowing: the difference between two
int64timestamps is cast tointduring sorting. - Silent read failure: failure to read an individual snapshot’s
info.jsonreturns an invalid object without logging the underlying error. - No JSON-root validation: parser results are accessed without explicitly validating the root-node type.
- Unmount cleanup: the unmount helper does not remove the temporary mount directory or clear the result’s mount state.
- Repository-empty restriction: migration is refused when a valid configuration and repository exist but no snapshots are present.
40. Recommended Migration Invariants
For a reliable migration, the implementation should preserve the following invariants:
- The source and destination configuration paths must be different when source removal is enabled
- The source and destination repository paths must be different before a rename is attempted
- Detection mounts must be unmounted on every return path
- Metadata changes should be written atomically
- A failed metadata or configuration update should affect the final migration result
- The original repository and control files should remain recoverable until migration completes
- Temporary mount paths should be process-specific
- BTRFS top-level filesystems should be mounted explicitly when repository paths reside outside the default subvolume
41. Licensing
The source header identifies the module as part of the BastionGuard Backup fork and declares:
BastionGuard Backup
Copyright (C) 2025–2026 Calogero Scarnà
GPL-2.0-or-later
The module is distributed under the GNU General Public License version 2 or any later version.
