Skip to content

copy: copy an archive to a new archive name, #2300 - #10221

Open
ThomasWaldmann wants to merge 1 commit into
borgbackup:masterfrom
ThomasWaldmann:archive-copy-2300
Open

copy: copy an archive to a new archive name, #2300#10221
ThomasWaldmann wants to merge 1 commit into
borgbackup:masterfrom
ThomasWaldmann:archive-copy-2300

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

Adds a borg copy OLDNAME NEWNAME command, addressing #2300 (the "additional names for an existing archive" request).

Rationale

#2300 asked for archive aliases, sketched as a name -> archive dict in the manifest. In borg2 that design is both impossible and unnecessary:

  • Impossible: each archive is a archives/<hex-id> entry in borgstore, keyed by the id of the archive metadata object, and the name lives inside that id-hashed object. Two names cannot point at one archive object.
  • Unnecessary: borg2 does not refcount chunks. borg compact only frees chunks that no remaining archive references, so two archives sharing all their chunks via dedup already behave the way the issue wanted -- deleting one leaves the other fully intact, with no dangling-pointer failure mode that a symlink-style alias would have.

So instead of an alias, this creates a real, independent second archive -- which costs almost nothing.

Implementation

Archive.copy() is Archive.rename() without removing the original archive entry: it writes a new archive metadata object (via set_meta("name", ...)) and leaves the old directory entry in place. No file content is read or written, and item_ptrs is carried over unchanged, so the item metadata stream and every content chunk are shared.

The command is modelled on rename_cmd.py and uses @with_archive, so OLDNAME resolution (name if unique, or aid:<hex> prefix) and aid:-aware shell completion come for free.

Copying an archive to its own name is refused: the new metadata would be byte-identical, hence the same archive id, hence it would just overwrite the existing directory entry -- a silent no-op rather than a copy.

Measured behaviour

On a repo with a single 2 MB random-data archive:

step repo size
after borg create 2036 KB
after borg copy 2044 KB
after borg delete <original> + borg compact 2048 KB

A control run without the copy drops to 24 KB at the last step, so compact really is freeing the data in the absence of a second referrer. After deleting the original, borg check is clean and the copy extracts byte-identical.

Tests

Seven tests in src/borg/testsuite/archiver/copy_cmd_test.py: basic copy, copy by aid:, shared item_ptrs/timestamp, delete-original-then-compact-then-extract, copying into an existing archive series, copy-to-same-name refused, ambiguous OLDNAME refused.

Notes for review

  • docs/man/borg-copy.1 is included (following the borg analyze commit); the date-only drift build_man produces in all the other man pages is not.
  • The changelog entry went under 2.0.0b24.

@ThomasWaldmann ThomasWaldmann changed the title copy: new command to copy an archive to a new archive name, #2300 copy: copy an archive to a new archive name, #2300 Aug 27, 2026
…p#2300

Copying is cheap and fast: no file content is read or written, only a new
archive metadata object is created. Like any deduplicated archives, the two
archives share their data, so a copy needs almost no additional repository
space.

The copy is an independent archive: deleting either of the two archives
keeps the other one intact, because "borg compact" only frees chunks that no
remaining archive references.

Archive.copy() is Archive.rename() without removing the original archive
entry. Copying an archive to its own name is refused: the new metadata would
be identical, thus have the same archive ID, and no second archive would be
created.

OLDNAME must match precisely one archive, so it accepts an archive name (if
it is unique) or an archive ID like "aid:d34db33f".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.31%. Comparing base (0ad66ed) to head (9b89c94).
⚠️ Report is 34 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10221      +/-   ##
==========================================
+ Coverage   87.29%   87.31%   +0.02%     
==========================================
  Files         102      103       +1     
  Lines       18444    18480      +36     
  Branches     2834     2835       +1     
==========================================
+ Hits        16100    16136      +36     
  Misses       1638     1638              
  Partials      706      706              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant