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 =========================================== diff --git a/src/borg_import/rsync_tmbackup.py b/src/borg_import/rsync_tmbackup.py index a177578..ddd9254 100644 --- a/src/borg_import/rsync_tmbackup.py +++ b/src/borg_import/rsync_tmbackup.py @@ -10,26 +10,40 @@ 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(): - raise FileNotFoundError("The backup.marker file must exist for rsync-time-backup import") + if not (root / "backup.marker").exists(): + 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 diff --git a/src/borg_import/testsuite/test_borg.py b/src/borg_import/testsuite/test_borg.py index 0c51101..c41d85d 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,67 @@ 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) +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