TimeshiftImport

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 from info.json
  • date – 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 UUID
  • sys_distro – distribution description stored in the snapshot
  • tags – snapshot tag string copied from the control file
  • description – 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 configuration
  • valid – becomes true after a readable and parseable info.json has 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 found
  • config_path – path of the discovered JSON configuration
  • backup_uuid – configured backup-device UUID
  • backup_device – resolved device path, such as /dev/sda2
  • mount_path – temporary mount location used during scanning
  • is_btrfs – configured snapshot-engine mode
  • snapshots_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 repository
  • migrate() – 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:

  1. Create an empty detection result
  2. Search for a supported configuration file
  3. Read the backup UUID and BTRFS mode
  4. Resolve the backup device from its UUID
  5. Mount the device in read-only mode
  6. Calculate the RSYNC or BTRFS snapshot path
  7. Scan snapshot directories
  8. Parse each snapshot’s info.json
  9. Sort valid snapshots from newest to oldest
  10. 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_uuid is 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_uuid
  • sys_distro
  • tags
  • description
  • date_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.found is false
  • 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:

  1. Locate the configuration file
  2. Set the configuration-presence flag
  3. Parse the backup UUID and mode
  4. Resolve the device through /dev/disk/by-uuid or blkid
  5. Create the fixed runtime mount point
  6. Mount the filesystem read-only
  7. Select the RSYNC or BTRFS repository path
  8. Enumerate snapshot directories
  9. Parse each info.json
  10. Discard invalid entries
  11. Sort valid entries from newest to oldest
  12. Return the detection result

36. Migration Data Flow

The complete migration flow is:

  1. Validate the previous detection result
  2. Remount the backup filesystem as read-write
  3. Calculate source and destination repository paths
  4. Rename the repository directory
  5. Update each imported snapshot path
  6. Rewrite each available info.json
  7. Update or create the BastionGuard configuration
  8. Optionally remove the source configuration
  9. Unmount the backup filesystem
  10. 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.json metadata 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-from member.
  • 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_CONF and BGBACKUP_CONF contain exactly the same path.
  • Identical repository paths: both the old and new RSYNC paths are bastionguard-backup, while both BTRFS paths are bastionguard-backup-btrfs.
  • No-op rename: the migration currently attempts to rename a repository directory to the same path.
  • Configuration removal conflict: enabling remove_timeshift_config can remove the same canonical configuration file that update_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: found is 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 blkid fallback.
  • Nested exception handling: mount and remount operations contain redundant nested try/catch blocks for GLib.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.json files are not copied before modification.
  • Partial success reporting: configuration-write and per-snapshot metadata errors do not cause migrate() to return false.
  • Unused parent window: the parent_window parameter of migrate() is not referenced.
  • String-encoded Boolean values: btrfs_mode and imported_from_timeshift are written as JSON strings instead of native Boolean members.
  • Comparator narrowing: the difference between two int64 timestamps is cast to int during sorting.
  • Silent read failure: failure to read an individual snapshot’s info.json returns 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.