Skip to content
Merged
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
5 changes: 5 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ New features:
- locking: tell who is holding a lock we wait for or time out on: lock type, host,
pid and age of that lock. Also name the repository in the message (instead of just
the storage backend) and log which locks ``borg break-lock`` breaks, #2261
- new "borg copy OLDNAME NEWNAME" command: copy an archive to a new archive name, #2300.
Copying is cheap: no file content is read or written, only a new archive metadata
object is created, and the two archives share their data like any other deduplicated
archives do. The copy is an independent archive: deleting either of the two archives
keeps the other one intact.

Fixes:

Expand Down
103 changes: 103 additions & 0 deletions docs/man/borg-copy.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.\" Man page generated from reStructuredText
.\" by the Docutils 0.22.4 manpage writer.
.
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "borg-copy" "1" "2026-08-28" "" "borg backup tool"
.SH Name
borg-copy \- Copy an archive to a new archive name.
.SH SYNOPSIS
.sp
borg [common options] copy [options] OLDNAME NEWNAME
.SH DESCRIPTION
.sp
This command copies an existing archive to a new archive with a different name,
keeping the existing archive.
.sp
Afterwards, the repository has two archives with the same contents, but with
different names and different archive IDs. The copy is an independent archive:
deleting either of the two archives keeps the other one intact, because
\fBborg compact\fP only frees chunks that no remaining archive references.
.sp
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.
.sp
Because archive names do not need to be unique, NEWNAME may also be the name of
some \fIother\fP already existing archive \- the copy then just becomes another archive
of that archive series.
.sp
NEWNAME must be different from the name of the archive that is copied, though: the
copy would get identical metadata and thus the same archive ID as its source, so no
second archive could be created.
.sp
OLDNAME must match precisely one archive: give an archive name (if it is unique) or
an archive ID, like \fBaid:d34db33f\fP\&.
.sp
Note: to copy archives into a \fIdifferent\fP repository, use \fBborg transfer\fP\&.
.SH OPTIONS
.sp
See \fIborg\-common(1)\fP for common options of Borg commands.
.SS arguments
.INDENT 0.0
.TP
.B OLDNAME
specify the existing archive name or ID
.TP
.B NEWNAME
specify the new archive name
.UNINDENT
.SH EXAMPLES
.INDENT 0.0
.INDENT 3.5
.sp
.EX
# create an archive, then keep a copy of it under a stable name:
$ borg create backup\-2016\-02\-15 ~
$ borg copy backup\-2016\-02\-15 known\-good
$ borg repo\-list
e6a2b1c4 Mon, 2016\-02\-15 19:50:19 +0100 backup\-2016\-02\-15 tw MacBook\-Pro
9f3d0a77 Mon, 2016\-02\-15 19:50:19 +0100 known\-good tw MacBook\-Pro

# the copy is an independent archive:
# after deleting (and compacting away) the original, the copy is still complete.
$ borg delete backup\-2016\-02\-15
$ borg compact
$ borg extract known\-good

# if the archive name is not unique, address the archive by its ID:
$ borg copy aid:e6a2b1c4 known\-good
.EE
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fIborg\-common(1)\fP
.SH Author
The Borg Collective
.\" End of generated man page.
1 change: 1 addition & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Usage
usage/find
usage/tag
usage/rename
usage/copy
usage/diff
usage/delete
usage/prune
Expand Down
21 changes: 21 additions & 0 deletions docs/usage/copy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.. include:: copy.rst.inc

Examples
~~~~~~~~
::

# create an archive, then keep a copy of it under a stable name:
$ borg create backup-2016-02-15 ~
$ borg copy backup-2016-02-15 known-good
$ borg repo-list
e6a2b1c4 Mon, 2016-02-15 19:50:19 +0100 backup-2016-02-15 tw MacBook-Pro
9f3d0a77 Mon, 2016-02-15 19:50:19 +0100 known-good tw MacBook-Pro

# the copy is an independent archive:
# after deleting (and compacting away) the original, the copy is still complete.
$ borg delete backup-2016-02-15
$ borg compact
$ borg extract known-good

# if the archive name is not unique, address the archive by its ID:
$ borg copy aid:e6a2b1c4 known-good
72 changes: 72 additions & 0 deletions docs/usage/copy.rst.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!

.. _borg_copy:

borg copy
---------
.. code-block:: none

borg [common options] copy [options] OLDNAME NEWNAME

.. only:: html

.. class:: borg-options-table

+-------------------------------------------------------+-------------+-----------------------------------------+
| **positional arguments** |
+-------------------------------------------------------+-------------+-----------------------------------------+
| | ``OLDNAME`` | specify the existing archive name or ID |
+-------------------------------------------------------+-------------+-----------------------------------------+
| | ``NEWNAME`` | specify the new archive name |
+-------------------------------------------------------+-------------+-----------------------------------------+
| .. class:: borg-common-opt-ref |
| |
| :ref:`common_options` |
+-------------------------------------------------------+-------------+-----------------------------------------+

.. raw:: html

<script type='text/javascript'>
$(document).ready(function () {
$('.borg-options-table colgroup').remove();
})
</script>

.. only:: latex

OLDNAME
specify the existing archive name or ID
NEWNAME
specify the new archive name


:ref:`common_options`
|

Description
~~~~~~~~~~~

This command copies an existing archive to a new archive with a different name,
keeping the existing archive.

Afterwards, the repository has two archives with the same contents, but with
different names and different archive IDs. 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.

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.

Because archive names do not need to be unique, NEWNAME may also be the name of
some *other* already existing archive - the copy then just becomes another archive
of that archive series.

NEWNAME must be different from the name of the archive that is copied, though: the
copy would get identical metadata and thus the same archive ID as its source, so no
second archive could be created.

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

Note: to copy archives into a *different* repository, use ``borg transfer``.
5 changes: 3 additions & 2 deletions docs/usage/rename.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Examples

$ borg create archivename ~
$ borg repo-list
archivename Mon, 2016-02-15 19:50:19
8df049de Mon, 2016-02-15 19:50:19 +0100 archivename tw MacBook-Pro

# renaming rewrites the archive metadata, so the archive ID changes:
$ borg rename archivename newname
$ borg repo-list
newname Mon, 2016-02-15 19:50:19
69ea925b Mon, 2016-02-15 19:50:19 +0100 newname tw MacBook-Pro

27 changes: 27 additions & 0 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,33 @@ def rename(self, name):
self.set_meta("name", name)
self.manifest.archives.delete_by_id(old_id)

def copy(self, name):
"""Copy this archive to a new archive with the given name, keeping this archive.

This is like .rename(), but the original archive entry is not removed, so afterwards the
repository has two archives with identical contents under two different names (and two
different archive IDs).

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

Copying is cheap: no file content is read or written, only a new archive metadata object
is created. The item metadata stream and the file content chunks are shared between the
two archives (like they are between any deduplicated archives).

The new name may be the name of some other existing archive (archive names do not need
to be unique), but it must be different from this archive's name, see below.

Afterwards, this Archive instance refers to the new archive (new name, new archive ID).
"""
if name == self.name:
# the new metadata would be identical, thus have the same archive ID and just overwrite
# the existing archives directory entry - no second archive would be created.
raise Error(f"Archive {name} can not be copied to the same name.")
Comment thread
ThomasWaldmann marked this conversation as resolved.
self.name = name
self.set_meta("name", name)

def delete(self):
# quick and dirty: we just nuke the archive from the archives list - that will
# potentially orphan all chunks previously referenced by the archive, except the ones also
Expand Down
3 changes: 3 additions & 0 deletions src/borg/archiver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
from .check_cmd import CheckMixIn
from .compact_cmd import CompactMixIn
from .completion_cmd import CompletionMixIn
from .copy_cmd import CopyMixIn
from .create_cmd import CreateMixIn
from .debug_cmd import DebugMixIn
from .delete_cmd import DeleteMixIn
Expand Down Expand Up @@ -105,6 +106,7 @@ class Archiver(
CheckMixIn,
CompactMixIn,
CompletionMixIn,
CopyMixIn,
CreateMixIn,
DebugMixIn,
DeleteMixIn,
Expand Down Expand Up @@ -298,6 +300,7 @@ def build_parser(self):
self.build_parser_check(subparsers, common_parser, mid_common_parser)
self.build_parser_compact(subparsers, common_parser, mid_common_parser)
self.build_parser_completion(subparsers, common_parser, mid_common_parser)
self.build_parser_copy(subparsers, common_parser, mid_common_parser)
self.build_parser_create(subparsers, common_parser, mid_common_parser)
self.build_parser_debug(subparsers, common_parser, mid_common_parser)
self.build_parser_delete(subparsers, common_parser, mid_common_parser)
Expand Down
60 changes: 60 additions & 0 deletions src/borg/archiver/copy_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from ._common import with_repository, with_archive
from ..constants import * # NOQA
from ..helpers import archivename_validator, bin_to_hex
from ..helpers.argparsing import ArgumentParser
from ..manifest import Manifest

from ..logger import create_logger

logger = create_logger()


class CopyMixIn:
@with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,))
@with_archive
def do_copy(self, args, repository, manifest, cache, archive):
"""Copy an archive to a new archive name."""
old_id = archive.id
archive.copy(args.newname)
manifest.write()
logger.info(f"id: {bin_to_hex(old_id):.8} -> {bin_to_hex(archive.id):.8}, name: {archive.name}.")

def build_parser_copy(self, subparsers, common_parser, mid_common_parser):
from ._common import process_epilog

copy_epilog = process_epilog(
"""
This command copies an existing archive to a new archive with a different name,
keeping the existing archive.

Afterwards, the repository has two archives with the same contents, but with
different names and different archive IDs. 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.

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.

Because archive names do not need to be unique, NEWNAME may also be the name of
some *other* already existing archive - the copy then just becomes another archive
of that archive series.

NEWNAME must be different from the name of the archive that is copied, though: the
copy would get identical metadata and thus the same archive ID as its source, so no
second archive could be created.

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

Note: to copy archives into a *different* repository, use ``borg transfer``.
"""
)
subparser = ArgumentParser(parents=[common_parser], description=self.do_copy.__doc__, epilog=copy_epilog)
subparsers.add_subcommand("copy", subparser, help="copy an archive to a new archive name")
subparser.add_argument(
"name", metavar="OLDNAME", type=archivename_validator, help="specify the existing archive name or ID"
)
subparser.add_argument(
"newname", metavar="NEWNAME", type=archivename_validator, help="specify the new archive name"
)
Loading
Loading