From a02259846ceeca556f9d076eeec20c81626286b1 Mon Sep 17 00:00:00 2001 From: hiijoshi Date: Sat, 21 Mar 2026 11:36:22 +0530 Subject: [PATCH 1/5] Add beginner-friendly quick start section to README --- README.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.rst b/README.rst index b1a44c4..9831cfa 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,38 @@ What is borg-import? borg-import converts backups made with other backup software into the format used by `BorgBackup `_. See ``borg-import -h`` for more information. +Quick Start (Beginner Guide) +=========================== + +This section helps new users quickly install and use borg-import. + +Installation +------------ + +1. Install BorgBackup: + + On macOS (using Homebrew): + + ``brew install borgbackup`` + +2. Clone borg-import and install: + + ``git clone https://github.com/borgbackup/borg-import.git`` + ``cd borg-import`` + ``python3 -m venv .venv`` + ``source .venv/bin/activate`` + ``pip install -e .`` + +Basic Usage Example +------------------- + +Import backups from rsnapshot: + +``borg-import rsnapshot /path/to/snapshots /path/to/borg-repo`` + +Get help for any command: + +``borg-import -h`` Potential advantages over doing it manually =========================================== From d744f23805fd95f9cc79aa2aa8334e395bc082b6 Mon Sep 17 00:00:00 2001 From: hiijoshi Date: Sat, 21 Mar 2026 11:54:06 +0530 Subject: [PATCH 2/5] Add basic test for rsynchl importer --- src/borg_import/testsuite/test_borg.py | 31 ++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/borg_import/testsuite/test_borg.py b/src/borg_import/testsuite/test_borg.py index 0c51101..6503a2e 100644 --- a/src/borg_import/testsuite/test_borg.py +++ b/src/borg_import/testsuite/test_borg.py @@ -25,7 +25,6 @@ def test_borg_import(tmpdir, monkeypatch): # Create archives in the source repository subprocess.check_call(["borg", "create", f"{source_repo}::archive1", "."], cwd=str(archive1_data)) - subprocess.check_call(["borg", "create", f"{source_repo}::archive2", "."], cwd=str(archive2_data)) # Initialize the target repository @@ -49,7 +48,6 @@ def test_borg_import(tmpdir, monkeypatch): extract_dir2 = tmpdir.mkdir("extract2") subprocess.check_call(["borg", "extract", f"{target_repo}::archive1"], cwd=str(extract_dir1)) - subprocess.check_call(["borg", "extract", f"{target_repo}::archive2"], cwd=str(extract_dir2)) # Verify the contents of the extracted archives @@ -57,3 +55,32 @@ def test_borg_import(tmpdir, monkeypatch): assert extract_dir1.join("file2.txt").read() == "This is file 2 in archive 1" assert extract_dir2.join("file1.txt").read() == "This is file 1 in archive 2" assert extract_dir2.join("file2.txt").read() == "This is file 2 in archive 2" + + +def test_rsynchl_import(tmpdir, monkeypatch): + source_dir = tmpdir.mkdir("rsync_source") + target_repo = tmpdir.mkdir("target_repo") + + # create folders simulating rsync backups + archive1 = source_dir.mkdir("backup1") + archive2 = source_dir.mkdir("backup2") + + archive1.join("file.txt").write("hello1") + archive2.join("file.txt").write("hello2") + + subprocess.check_call(["borg", "init", "--encryption=none", str(target_repo)]) + + monkeypatch.setattr("sys.argv", [ + "borg-import", + "rsynchl", + str(source_dir), + str(target_repo) + ]) + + main() + + output = subprocess.check_output(["borg", "list", "--short", str(target_repo)]).decode() + archives = output.splitlines() + + assert len(archives) >= 1 + assert any("backup1" in a or "backup2" in a for a in archives) \ No newline at end of file From 5ea6530d1a6e35f52681bf6d292618c054e09830 Mon Sep 17 00:00:00 2001 From: hiijoshi Date: Sat, 21 Mar 2026 12:08:15 +0530 Subject: [PATCH 3/5] Add test for rsync_tmbackup importer --- src/borg_import/rsync_tmbackup.py | 2 +- src/borg_import/testsuite/test_borg.py | 37 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/borg_import/rsync_tmbackup.py b/src/borg_import/rsync_tmbackup.py index a177578..edb9431 100644 --- a/src/borg_import/rsync_tmbackup.py +++ b/src/borg_import/rsync_tmbackup.py @@ -10,7 +10,7 @@ def get_tmbackup_snapshots(root, prefix): """Return metadata for all snapshots discovered in the rsync root directory.""" regex = re.compile(r"(?P.+)") - if not Path("backup.marker").exists(): + if not (root / "backup.marker").exists(): raise FileNotFoundError("The backup.marker file must exist for rsync-time-backup import") for path in discover(str(root), 1): diff --git a/src/borg_import/testsuite/test_borg.py b/src/borg_import/testsuite/test_borg.py index 6503a2e..c41d85d 100644 --- a/src/borg_import/testsuite/test_borg.py +++ b/src/borg_import/testsuite/test_borg.py @@ -83,4 +83,39 @@ def test_rsynchl_import(tmpdir, monkeypatch): archives = output.splitlines() assert len(archives) >= 1 - assert any("backup1" in a or "backup2" in a for a in archives) \ No newline at end of file + assert any("backup1" in a or "backup2" in a for a in archives) +def test_rsync_tmbackup_import(tmpdir, monkeypatch): + import subprocess + from borg_import.main import main + + source_dir = tmpdir.mkdir("tmbackup_source") + target_repo = tmpdir.mkdir("target_repo") + + # required marker file for rsync-time-backup imports + source_dir.join("backup.marker").write("") + + # simulate rsync-time-backup style folder names + archive1 = source_dir.mkdir("2024-01-01-000000") + archive2 = source_dir.mkdir("2024-01-02-000000") + + archive1.join("file.txt").write("hello1") + archive2.join("file.txt").write("hello2") + + subprocess.check_call(["borg", "init", "--encryption=none", str(target_repo)]) + + monkeypatch.setattr("sys.argv", [ + "borg-import", + "rsync_tmbackup", + "--prefix=backup-", + str(source_dir), + str(target_repo), + ]) + + main() + + output = subprocess.check_output( + ["borg", "list", "--short", str(target_repo)] + ).decode() + archives = output.splitlines() + + assert len(archives) >= 1 \ No newline at end of file From 13f55ba9929f2079904da88501605b706b5b3e79 Mon Sep 17 00:00:00 2001 From: hiijoshi Date: Sat, 21 Mar 2026 12:20:00 +0530 Subject: [PATCH 4/5] Improve backup.marker error message for rsync_tmbackup importer --- src/borg_import/rsync_tmbackup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/borg_import/rsync_tmbackup.py b/src/borg_import/rsync_tmbackup.py index edb9431..811da47 100644 --- a/src/borg_import/rsync_tmbackup.py +++ b/src/borg_import/rsync_tmbackup.py @@ -11,8 +11,7 @@ def get_tmbackup_snapshots(root, prefix): regex = re.compile(r"(?P.+)") if not (root / "backup.marker").exists(): - raise FileNotFoundError("The backup.marker file must exist for rsync-time-backup import") - + raise FileNotFoundError(f"The backup.marker file must exist inside the provided rsync-time-backup root directory: {root}") for path in discover(str(root), 1): parsed = parser(path, regex) if parsed is not None and parsed["snapshot_date"] not in ("latest",): From fee531405e1c51c6b6e71aab3c47896c5c3c8d5d Mon Sep 17 00:00:00 2001 From: hiijoshi Date: Sat, 21 Mar 2026 14:36:45 +0530 Subject: [PATCH 5/5] Improve backup.marker error message for rsync_tmbackup importer --- src/borg_import/rsync_tmbackup.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/borg_import/rsync_tmbackup.py b/src/borg_import/rsync_tmbackup.py index 811da47..ddd9254 100644 --- a/src/borg_import/rsync_tmbackup.py +++ b/src/borg_import/rsync_tmbackup.py @@ -11,24 +11,39 @@ def get_tmbackup_snapshots(root, prefix): regex = re.compile(r"(?P.+)") if not (root / "backup.marker").exists(): - raise FileNotFoundError(f"The backup.marker file must exist inside the provided rsync-time-backup root directory: {root}") + raise FileNotFoundError( + f"The backup.marker file must exist inside the provided rsync-time-backup root directory: {root}" + ) + for path in discover(str(root), 1): parsed = parser(path, regex) if parsed is not None and parsed["snapshot_date"] not in ("latest",): abs_path = root / path + + snapshot_date = parsed["snapshot_date"] + if prefix and snapshot_date.startswith(prefix): + snapshot_date = snapshot_date[len(prefix):] + meta = dict( name=make_name("".join([prefix, parsed["snapshot_date"]])), path=abs_path, - timestamp=datetime_from_string(path), + timestamp=datetime_from_string(snapshot_date), ) yield meta - elif parsed["snapshot_date"] in ("latest",): + + elif parsed is not None and parsed["snapshot_date"] in ("latest",): # "latest" is a symlink to the most recent backup. Import it anyway # in case the user wants to do borg mount or has existing references # to "latest". abs_path = root / path - timestamp = Path("latest").resolve().name + timestamp = (root / "latest").resolve().name + + if prefix and timestamp.startswith(prefix): + timestamp = timestamp[len(prefix):] + meta = dict( - name=make_name("".join([prefix, "latest"])), path=abs_path, timestamp=datetime_from_string(timestamp) + name=make_name("".join([prefix, "latest"])), + path=abs_path, + timestamp=datetime_from_string(timestamp), ) - yield meta + yield meta \ No newline at end of file