Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@ What is borg-import?
borg-import converts backups made with other backup software into the format used by `BorgBackup <https://github.com/borgbackup/borg>`_.

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
===========================================
Expand Down
28 changes: 21 additions & 7 deletions src/borg_import/rsync_tmbackup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<snapshot_date>.+)")

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
66 changes: 64 additions & 2 deletions src/borg_import/testsuite/test_borg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,11 +48,74 @@ 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
assert extract_dir1.join("file1.txt").read() == "This is file 1 in archive 1"
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