Переглянути джерело

remove remainders of append-only support

Some features like append-only repositories rely on a server-side component
that enforces them (because that shall only be controllable server-side,
not client-side).

So, that can only work, if such a server-side component exists, which is the
case for borg 1.x ssh: repositories (but not for borg 1.x non-ssh: repositories).

For borg2, we currently have:
- fs repos
- sftp: repos
- rclone: repos (enabling many different cloud providers)
- s3/b3: repos
- ssh: repos using client/server rpc code similar as in borg 1.x

So, only for the last method we have a borg server-side process that could enforce some features, but not for any of the other repo types.

For append-only the current idea is that this should not be done within borg,
but solved by a missing repo object delete permission enforced by the storage.

borg create could then use credentials that miss permission to delete,
while borg compact would use credentials that include permission to delete.
Thomas Waldmann 1 місяць тому
батько
коміт
9e6d90754e

+ 0 - 10
docs/deployment/hosting-repositories.rst

@@ -55,16 +55,6 @@ multiple times to permit access to more than one repository.
 The repository may not exist yet; it can be initialized by the user,
 The repository may not exist yet; it can be initialized by the user,
 which allows for encryption.
 which allows for encryption.
 
 
-**Specificities: Append-only repositories**
-
-Running ``borg init`` via a ``borg serve --append-only`` server will **not**
-create a repository that is configured to be append-only by its repository
-config.
-
-But, ``--append-only`` arguments in ``authorized_keys`` will override the
-repository config, therefore append-only mode can be enabled on a key by key
-basis.
-
 Refer to the `sshd(8) <https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8>`_
 Refer to the `sshd(8) <https://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8>`_
 man page for more details on SSH options.
 man page for more details on SSH options.
 See also :ref:`borg_serve`
 See also :ref:`borg_serve`

+ 4 - 6
docs/deployment/pull-backup.rst

@@ -247,7 +247,7 @@ to *borg-client* has to have read and write permissions on ``/run/borg``::
 On *borg-server*, we have to start the command ``borg serve`` and make its
 On *borg-server*, we have to start the command ``borg serve`` and make its
 standard input and output available to a unix socket::
 standard input and output available to a unix socket::
 
 
-   borg-server:~$ socat UNIX-LISTEN:/run/borg/reponame.sock,fork EXEC:"borg serve --append-only --restrict-to-path /path/to/repo"
+   borg-server:~$ socat UNIX-LISTEN:/run/borg/reponame.sock,fork EXEC:"borg serve --restrict-to-path /path/to/repo"
 
 
 Socat will wait until a connection is opened. Then socat will execute the
 Socat will wait until a connection is opened. Then socat will execute the
 command given, redirecting Standard Input and Output to the unix socket. The
 command given, redirecting Standard Input and Output to the unix socket. The
@@ -350,7 +350,7 @@ dedicated ssh key:
 
 
   borgs@borg-server$ install -m 700 -d ~/.ssh/
   borgs@borg-server$ install -m 700 -d ~/.ssh/
   borgs@borg-server$ ssh-keygen -N '' -t rsa  -f ~/.ssh/borg-client_key
   borgs@borg-server$ ssh-keygen -N '' -t rsa  -f ~/.ssh/borg-client_key
-  borgs@borg-server$ { echo -n 'command="borg serve --append-only --restrict-to-repo ~/repo",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys
+  borgs@borg-server$ { echo -n 'command="borg serve --restrict-to-repo ~/repo",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys
   borgs@borg-server$ chmod 600 ~/.ssh/authorized_keys
   borgs@borg-server$ chmod 600 ~/.ssh/authorized_keys
 
 
 ``install -m 700 -d ~/.ssh/``
 ``install -m 700 -d ~/.ssh/``
@@ -365,12 +365,10 @@ dedicated ssh key:
   Another more complex approach is using a unique ssh key for each pull operation.
   Another more complex approach is using a unique ssh key for each pull operation.
   This is more secure as it guarantees that the key will not be used for other purposes.
   This is more secure as it guarantees that the key will not be used for other purposes.
 
 
-``{ echo -n 'command="borg serve --append-only --restrict-to-repo ~/repo",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys``
+``{ echo -n 'command="borg serve --restrict-to-repo ~/repo",restrict '; cat ~/.ssh/borg-client_key.pub; } >> ~/.ssh/authorized_keys``
 
 
   Add borg-client's ssh public key to ~/.ssh/authorized_keys with forced command and restricted mode.
   Add borg-client's ssh public key to ~/.ssh/authorized_keys with forced command and restricted mode.
-  The borg client is restricted to use one repo at the specified path and to append-only operation.
-  Commands like *delete*, *prune* and *compact* have to be executed another way, for example directly on *borg-server*
-  side or from a privileged, less restricted client (using another authorized_keys entry).
+  The borg client is restricted to use one repo at the specified path.
 
 
 ``chmod 600 ~/.ssh/authorized_keys``
 ``chmod 600 ~/.ssh/authorized_keys``
 
 

+ 2 - 10
docs/faq.rst

@@ -146,13 +146,6 @@ How can I restore huge file(s) over an unstable connection?
 Try using ``borg mount`` and ``rsync`` (or a similar tool that supports
 Try using ``borg mount`` and ``rsync`` (or a similar tool that supports
 resuming a partial file copy from what's already copied).
 resuming a partial file copy from what's already copied).
 
 
-How can I switch append-only mode on and off?
----------------------------------------------
-
-You could do that (via borg config REPO append_only 0/1), but using different
-ssh keys and different entries in ``authorized_keys`` is much easier and also
-maybe has less potential of things going wrong somehow.
-
 My machine goes to sleep causing `Broken pipe`
 My machine goes to sleep causing `Broken pipe`
 ----------------------------------------------
 ----------------------------------------------
 
 
@@ -371,8 +364,8 @@ Another option is to not consider inode numbers in the files cache by passing
 Why are backups slow on a Linux server that is a member of a Windows domain?
 Why are backups slow on a Linux server that is a member of a Windows domain?
 ----------------------------------------------------------------------------
 ----------------------------------------------------------------------------
 
 
-If a Linux server is a member of a Windows domain, username to userid resolution might be 
-performed via ``winbind`` without caching, which can slow down backups significantly. 
+If a Linux server is a member of a Windows domain, username to userid resolution might be
+performed via ``winbind`` without caching, which can slow down backups significantly.
 You can use e.g. ``nscd`` to add caching and improve the speed.
 You can use e.g. ``nscd`` to add caching and improve the speed.
 
 
 Security
 Security
@@ -553,7 +546,6 @@ C to delete all backups residing on S.
 
 
 These are your options to protect against that:
 These are your options to protect against that:
 
 
-- Do not allow to delete data permanently from the repo, see :ref:`append_only_mode`.
 - Use a pull-mode setup using ``ssh -R``, see :ref:`pull_backup` for more information.
 - Use a pull-mode setup using ``ssh -R``, see :ref:`pull_backup` for more information.
 - Mount C's filesystem on another machine and then create a backup of it.
 - Mount C's filesystem on another machine and then create a backup of it.
 - Do not give C filesystem-level access to S.
 - Do not give C filesystem-level access to S.

+ 0 - 131
docs/usage/notes.rst

@@ -177,10 +177,6 @@ Separate compaction
 Borg does not auto-compact the segment files in the repository at commit time
 Borg does not auto-compact the segment files in the repository at commit time
 (at the end of each repository-writing command) any more (since borg 1.2.0).
 (at the end of each repository-writing command) any more (since borg 1.2.0).
 
 
-This causes a similar behaviour of the repository as if it was in append-only
-mode (see below) most of the time (until ``borg compact`` is invoked or an
-old client triggers auto-compaction).
-
 This has some notable consequences:
 This has some notable consequences:
 
 
 - repository space is not freed immediately when deleting / pruning archives
 - repository space is not freed immediately when deleting / pruning archives
@@ -197,133 +193,6 @@ This has some notable consequences:
 
 
 You can manually run compaction by invoking the ``borg compact`` command.
 You can manually run compaction by invoking the ``borg compact`` command.
 
 
-.. _append_only_mode:
-
-Append-only mode (forbid compaction)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A repository can be made "append-only", which means that Borg will never
-overwrite or delete committed data (append-only refers to the segment files,
-but borg will also reject to delete the repository completely).
-
-If ``borg compact`` command is used on a repo in append-only mode, there
-will be no warning or error, but no compaction will happen.
-
-append-only is useful for scenarios where a backup client machine backups
-remotely to a backup server using ``borg serve``, since a hacked client machine
-cannot delete backups on the server permanently.
-
-To activate append-only mode, set ``append_only`` to 1 in the repository config:
-
-::
-
-    borg config append_only 1
-
-Note that you can go back-and-forth between normal and append-only operation with
-``borg config``; it's not a "one way trip."
-
-In append-only mode Borg will create a transaction log in the ``transactions`` file,
-where each line is a transaction and a UTC timestamp.
-
-In addition, ``borg serve`` can act as if a repository is in append-only mode with
-its option ``--append-only``. This can be very useful for fine-tuning access control
-in ``.ssh/authorized_keys``:
-
-::
-
-    command="borg serve --append-only ..." ssh-rsa <key used for not-always-trustable backup clients>
-    command="borg serve ..." ssh-rsa <key used for backup management>
-
-Running ``borg repo-create`` via a ``borg serve --append-only`` server will *not* create
-an append-only repository. Running ``borg repo-create --append-only`` creates an append-only
-repository regardless of server settings.
-
-Example
-+++++++
-
-Suppose an attacker remotely deleted all backups, but your repository was in append-only
-mode. A transaction log in this situation might look like this:
-
-::
-
-    transaction 1, UTC time 2016-03-31T15:53:27.383532
-    transaction 5, UTC time 2016-03-31T15:53:52.588922
-    transaction 11, UTC time 2016-03-31T15:54:23.887256
-    transaction 12, UTC time 2016-03-31T15:55:54.022540
-    transaction 13, UTC time 2016-03-31T15:55:55.472564
-
-From your security logs you conclude the attacker gained access at 15:54:00 and all
-the backups where deleted or replaced by compromised backups. From the log you know
-that transactions 11 and later are compromised. Note that the transaction ID is the
-name of the *last* file in the transaction. For example, transaction 11 spans files 6
-to 11.
-
-In a real attack you'll likely want to keep the compromised repository
-intact to analyze what the attacker tried to achieve. It's also a good idea to make this
-copy just in case something goes wrong during the recovery. Since recovery is done by
-deleting some files, a hard link copy (``cp -al``) is sufficient.
-
-The first step to reset the repository to transaction 5, the last uncompromised transaction,
-is to remove the ``hints.N``, ``index.N`` and ``integrity.N`` files in the repository (these
-files are always expendable). In this example N is 13.
-
-Then remove or move all segment files from the segment directories in ``data/`` starting
-with file 6::
-
-    rm data/**/{6..13}
-
-That's all to do in the repository.
-
-If you want to access this rolled back repository from a client that already has
-a cache for this repository, the cache will reflect a newer repository state
-than what you actually have in the repository now, after the rollback.
-
-Thus, you need to clear the cache::
-
-    borg repo-delete --cache-only
-
-The cache will get rebuilt automatically. Depending on repo size and archive
-count, it may take a while.
-
-You also will need to remove ~/.config/borg/security/REPOID/manifest-timestamp.
-
-Drawbacks
-+++++++++
-
-As data is only appended, and nothing removed, commands like ``prune`` or ``delete``
-won't free disk space, they merely tag data as deleted in a new transaction.
-
-Be aware that as soon as you write to the repo in non-append-only mode (e.g. prune,
-delete or create archives from an admin machine), it will remove the deleted objects
-permanently (including the ones that were already marked as deleted, but not removed,
-in append-only mode). Automated edits to the repository (such as a cron job running
-``borg prune``) will render append-only mode moot if data is deleted.
-
-Even if an archive appears to be available, it is possible an attacker could delete
-just a few chunks from an archive and silently corrupt its data. While in append-only
-mode, this is reversible, but ``borg check`` should be run before a writing/pruning
-operation on an append-only repository to catch accidental or malicious corruption::
-
-    # run without append-only mode
-    borg check --verify-data && borg compact
-
-Aside from checking repository & archive integrity you may also want to check
-backups manually to ensure their content seems correct.
-
-Further considerations
-++++++++++++++++++++++
-
-Append-only mode is not respected by tools other than Borg. ``rm`` still works on the
-repository. Make sure that backup client machines only get to access the repository via
-``borg serve``.
-
-Ensure that no remote access is possible if the repository is temporarily set to normal mode
-for e.g. regular pruning.
-
-Further protections can be implemented, but are outside of Borg's scope. For example,
-file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on
-new data files.
-
 SSH batch mode
 SSH batch mode
 ~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~
 
 

+ 0 - 2
scripts/shell_completions/fish/borg.fish

@@ -88,7 +88,6 @@ complete -c borg         -l 'rsh'                   -d 'Use COMMAND instead of s
 # borg init options
 # borg init options
 set -l encryption_modes "none keyfile keyfile-blake2 repokey repokey-blake2 authenticated authenticated-blake2"
 set -l encryption_modes "none keyfile keyfile-blake2 repokey repokey-blake2 authenticated authenticated-blake2"
 complete -c borg -f -s e -l 'encryption'            -d 'Encryption key MODE' -a "$encryption_modes" -n "__fish_seen_subcommand_from init"
 complete -c borg -f -s e -l 'encryption'            -d 'Encryption key MODE' -a "$encryption_modes" -n "__fish_seen_subcommand_from init"
-complete -c borg -f      -l 'append-only'           -d 'Create an append-only mode repository'      -n "__fish_seen_subcommand_from init"
 complete -c borg -f      -l 'make-parent-dirs'      -d 'Create parent directories'                  -n "__fish_seen_subcommand_from init"
 complete -c borg -f      -l 'make-parent-dirs'      -d 'Create parent directories'                  -n "__fish_seen_subcommand_from init"
 
 
 # borg create options
 # borg create options
@@ -314,7 +313,6 @@ complete -c borg -f      -l 'strip-components'      -d 'Remove NUMBER of leading
 # borg serve
 # borg serve
 complete -c borg         -l 'restrict-to-path'      -d 'Restrict repository access to PATH'         -n "__fish_seen_subcommand_from serve"
 complete -c borg         -l 'restrict-to-path'      -d 'Restrict repository access to PATH'         -n "__fish_seen_subcommand_from serve"
 complete -c borg         -l 'restrict-to-repository' -d 'Restrict repository access at PATH'        -n "__fish_seen_subcommand_from serve"
 complete -c borg         -l 'restrict-to-repository' -d 'Restrict repository access at PATH'        -n "__fish_seen_subcommand_from serve"
-complete -c borg -f      -l 'append-only'           -d 'Only allow appending to repository'         -n "__fish_seen_subcommand_from serve"
 
 
 # borg config
 # borg config
 complete -c borg -f -s c -l 'cache'                 -d 'Get/set/list values in the repo cache'      -n "__fish_seen_subcommand_from config"
 complete -c borg -f -s c -l 'cache'                 -d 'Get/set/list values in the repo cache'      -n "__fish_seen_subcommand_from config"

+ 1 - 1
src/borg/archiver/__init__.py

@@ -401,7 +401,7 @@ class Archiver(
                 # client is allowed to specify the allowlisted options,
                 # client is allowed to specify the allowlisted options,
                 # everything else comes from the forced "borg serve" command (or the defaults).
                 # everything else comes from the forced "borg serve" command (or the defaults).
                 # stuff from denylist must never be used from the client.
                 # stuff from denylist must never be used from the client.
-                denylist = {"restrict_to_paths", "restrict_to_repositories", "append_only", "umask"}
+                denylist = {"restrict_to_paths", "restrict_to_repositories", "umask"}
                 allowlist = {"debug_topics", "lock_wait", "log_level"}
                 allowlist = {"debug_topics", "lock_wait", "log_level"}
                 not_present = object()
                 not_present = object()
                 for attr_name in allowlist:
                 for attr_name in allowlist:

+ 4 - 17
src/borg/archiver/_common.py

@@ -30,29 +30,19 @@ from ..logger import create_logger
 logger = create_logger(__name__)
 logger = create_logger(__name__)
 
 
 
 
-def get_repository(location, *, create, exclusive, lock_wait, lock, append_only, args, v1_or_v2):
+def get_repository(location, *, create, exclusive, lock_wait, lock, args, v1_or_v2):
     if location.proto in ("ssh", "socket"):
     if location.proto in ("ssh", "socket"):
         RemoteRepoCls = LegacyRemoteRepository if v1_or_v2 else RemoteRepository
         RemoteRepoCls = LegacyRemoteRepository if v1_or_v2 else RemoteRepository
         repository = RemoteRepoCls(
         repository = RemoteRepoCls(
-            location,
-            create=create,
-            exclusive=exclusive,
-            lock_wait=lock_wait,
-            lock=lock,
-            append_only=append_only,
-            args=args,
+            location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock, args=args
         )
         )
 
 
     elif location.proto in ("sftp", "file", "rclone") and not v1_or_v2:  # stuff directly supported by borgstore
     elif location.proto in ("sftp", "file", "rclone") and not v1_or_v2:  # stuff directly supported by borgstore
-        repository = Repository(
-            location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock, append_only=append_only
-        )
+        repository = Repository(location, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock)
 
 
     else:
     else:
         RepoCls = LegacyRepository if v1_or_v2 else Repository
         RepoCls = LegacyRepository if v1_or_v2 else Repository
-        repository = RepoCls(
-            location.path, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock, append_only=append_only
-        )
+        repository = RepoCls(location.path, create=create, exclusive=exclusive, lock_wait=lock_wait, lock=lock)
     return repository
     return repository
 
 
 
 
@@ -114,7 +104,6 @@ def with_repository(
                 raise Error("missing repository, please use --repo or BORG_REPO env var!")
                 raise Error("missing repository, please use --repo or BORG_REPO env var!")
             assert isinstance(exclusive, bool)
             assert isinstance(exclusive, bool)
             lock = getattr(args, "lock", _lock)
             lock = getattr(args, "lock", _lock)
-            append_only = getattr(args, "append_only", False)
 
 
             repository = get_repository(
             repository = get_repository(
                 location,
                 location,
@@ -122,7 +111,6 @@ def with_repository(
                 exclusive=exclusive,
                 exclusive=exclusive,
                 lock_wait=self.lock_wait,
                 lock_wait=self.lock_wait,
                 lock=lock,
                 lock=lock,
-                append_only=append_only,
                 args=args,
                 args=args,
                 v1_or_v2=False,
                 v1_or_v2=False,
             )
             )
@@ -190,7 +178,6 @@ def with_other_repository(manifest=False, cache=False, compatibility=None):
                 exclusive=True,
                 exclusive=True,
                 lock_wait=self.lock_wait,
                 lock_wait=self.lock_wait,
                 lock=True,
                 lock=True,
-                append_only=False,
                 args=args,
                 args=args,
                 v1_or_v2=v1_or_v2,
                 v1_or_v2=v1_or_v2,
             )
             )

+ 1 - 12
src/borg/archiver/repo_create_cmd.py

@@ -4,7 +4,7 @@ from ._common import with_repository, with_other_repository, Highlander
 from ..cache import Cache
 from ..cache import Cache
 from ..constants import *  # NOQA
 from ..constants import *  # NOQA
 from ..crypto.key import key_creator, key_argument_names
 from ..crypto.key import key_creator, key_argument_names
-from ..helpers import CancelledByUser, CommandError
+from ..helpers import CancelledByUser
 from ..helpers import location_validator, Location
 from ..helpers import location_validator, Location
 from ..manifest import Manifest
 from ..manifest import Manifest
 
 
@@ -18,8 +18,6 @@ class RepoCreateMixIn:
     @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,))
     @with_other_repository(manifest=True, compatibility=(Manifest.Operation.READ,))
     def do_repo_create(self, args, repository, *, other_repository=None, other_manifest=None):
     def do_repo_create(self, args, repository, *, other_repository=None, other_manifest=None):
         """Create a new, empty repository"""
         """Create a new, empty repository"""
-        if args.append_only:
-            raise CommandError("append-only is not supported (yet?)")
         other_key = other_manifest.key if other_manifest is not None else None
         other_key = other_manifest.key if other_manifest is not None else None
         path = args.location.canonical_path()
         path = args.location.canonical_path()
         logger.info('Initializing repository at "%s"' % path)
         logger.info('Initializing repository at "%s"' % path)
@@ -224,15 +222,6 @@ class RepoCreateMixIn:
             action=Highlander,
             action=Highlander,
             help="select encryption key mode **(required)**",
             help="select encryption key mode **(required)**",
         )
         )
-        subparser.add_argument(
-            "--append-only",
-            dest="append_only",
-            action="store_true",
-            help="create an append-only mode repository. Note that this only affects "
-            "the low level structure of the repository, and running `delete` "
-            "or `prune` will still be allowed. See :ref:`append_only_mode` in "
-            "Additional Notes for more details.",
-        )
         subparser.add_argument(
         subparser.add_argument(
             "--copy-crypt-key",
             "--copy-crypt-key",
             dest="copy_crypt_key",
             dest="copy_crypt_key",

+ 0 - 2
src/borg/archiver/repo_info_cmd.py

@@ -36,7 +36,6 @@ class RepoInfoMixIn:
             Repository ID: {id}
             Repository ID: {id}
             Location: {location}
             Location: {location}
             Repository version: {version}
             Repository version: {version}
-            Append only: {append_only}
             {encryption}
             {encryption}
             """
             """
                 )
                 )
@@ -45,7 +44,6 @@ class RepoInfoMixIn:
                     id=bin_to_hex(repository.id),
                     id=bin_to_hex(repository.id),
                     location=repository._location.canonical_path(),
                     location=repository._location.canonical_path(),
                     version=repository.version,
                     version=repository.version,
-                    append_only=repository.append_only,
                     encryption=info["encryption"],
                     encryption=info["encryption"],
                 )
                 )
             )
             )

+ 0 - 13
src/borg/archiver/serve_cmd.py

@@ -1,7 +1,6 @@
 import argparse
 import argparse
 
 
 from ..constants import *  # NOQA
 from ..constants import *  # NOQA
-from ..helpers import CommandError
 from ..remote import RepositoryServer
 from ..remote import RepositoryServer
 
 
 from ..logger import create_logger
 from ..logger import create_logger
@@ -12,12 +11,9 @@ logger = create_logger()
 class ServeMixIn:
 class ServeMixIn:
     def do_serve(self, args):
     def do_serve(self, args):
         """Start in server mode. This command is usually not used manually."""
         """Start in server mode. This command is usually not used manually."""
-        if args.append_only:
-            raise CommandError("append-only is not supported (yet?)")
         RepositoryServer(
         RepositoryServer(
             restrict_to_paths=args.restrict_to_paths,
             restrict_to_paths=args.restrict_to_paths,
             restrict_to_repositories=args.restrict_to_repositories,
             restrict_to_repositories=args.restrict_to_repositories,
-            append_only=args.append_only,
             use_socket=args.use_socket,
             use_socket=args.use_socket,
         ).serve()
         ).serve()
 
 
@@ -71,12 +67,3 @@ class ServeMixIn:
             "PATH may be an empty directory or the last element of PATH may not exist, in which case "
             "PATH may be an empty directory or the last element of PATH may not exist, in which case "
             "the client may initialize a repository there.",
             "the client may initialize a repository there.",
         )
         )
-        subparser.add_argument(
-            "--append-only",
-            dest="append_only",
-            action="store_true",
-            help="only allow appending to repository segment files. Note that this only "
-            "affects the low level structure of the repository, and running `delete` "
-            "or `prune` will still be allowed. See :ref:`append_only_mode` in Additional "
-            "Notes for more details.",
-        )

+ 2 - 7
src/borg/legacyremote.py

@@ -245,9 +245,7 @@ class LegacyRemoteRepository:
         def required_version(self):
         def required_version(self):
             return self.args[1]
             return self.args[1]
 
 
-    def __init__(
-        self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None
-    ):
+    def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, args=None):
         self.location = self._location = location
         self.location = self._location = location
         self.preload_ids = []
         self.preload_ids = []
         self.msgid = 0
         self.msgid = 0
@@ -329,12 +327,10 @@ class LegacyRemoteRepository:
                 lock_wait=lock_wait,
                 lock_wait=lock_wait,
                 lock=lock,
                 lock=lock,
                 exclusive=exclusive,
                 exclusive=exclusive,
-                append_only=append_only,
                 v1_or_v2=True,  # make remote use LegacyRepository
                 v1_or_v2=True,  # make remote use LegacyRepository
             )
             )
             info = self.info()
             info = self.info()
             self.version = info["version"]
             self.version = info["version"]
-            self.append_only = info["append_only"]
 
 
         except Exception:
         except Exception:
             self.close()
             self.close()
@@ -625,10 +621,9 @@ class LegacyRemoteRepository:
 
 
     @api(
     @api(
         since=parse_version("1.0.0"),
         since=parse_version("1.0.0"),
-        append_only={"since": parse_version("1.0.7"), "previously": False},
         v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True},  # TODO fix version
         v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True},  # TODO fix version
     )
     )
-    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, v1_or_v2=False):
+    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, v1_or_v2=False):
         """actual remoting is done via self.call in the @api decorator"""
         """actual remoting is done via self.call in the @api decorator"""
 
 
     @api(since=parse_version("2.0.0a3"))
     @api(since=parse_version("2.0.0a3"))

+ 5 - 26
src/borg/legacyrepository.py

@@ -7,7 +7,6 @@ import struct
 import time
 import time
 from collections import defaultdict
 from collections import defaultdict
 from configparser import ConfigParser
 from configparser import ConfigParser
-from datetime import datetime, timezone
 from functools import partial
 from functools import partial
 from itertools import islice
 from itertools import islice
 from typing import Callable, DefaultDict
 from typing import Callable, DefaultDict
@@ -190,9 +189,7 @@ class LegacyRepository:
 
 
         exit_mcode = 21
         exit_mcode = 21
 
 
-    def __init__(
-        self, path, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, send_log_cb=None
-    ):
+    def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, send_log_cb=None):
         self.path = os.path.abspath(path)
         self.path = os.path.abspath(path)
         self._location = Location("file://%s" % self.path)
         self._location = Location("file://%s" % self.path)
         self.version = None
         self.version = None
@@ -218,7 +215,6 @@ class LegacyRepository:
         self.do_create = create
         self.do_create = create
         self.created = False
         self.created = False
         self.exclusive = exclusive
         self.exclusive = exclusive
-        self.append_only = append_only
         self.transaction_doomed = None
         self.transaction_doomed = None
         # v2 is the default repo version for borg 2.0
         # v2 is the default repo version for borg 2.0
         # v1 repos must only be used in a read-only way, e.g. for
         # v1 repos must only be used in a read-only way, e.g. for
@@ -327,7 +323,6 @@ class LegacyRepository:
         config.set("repository", "version", str(self.version))
         config.set("repository", "version", str(self.version))
         config.set("repository", "segments_per_dir", str(DEFAULT_SEGMENTS_PER_DIR))
         config.set("repository", "segments_per_dir", str(DEFAULT_SEGMENTS_PER_DIR))
         config.set("repository", "max_segment_size", str(DEFAULT_MAX_SEGMENT_SIZE))
         config.set("repository", "max_segment_size", str(DEFAULT_MAX_SEGMENT_SIZE))
-        config.set("repository", "append_only", str(int(self.append_only)))
         config.set("repository", "additional_free_space", "0")
         config.set("repository", "additional_free_space", "0")
         config.set("repository", "id", bin_to_hex(os.urandom(32)))
         config.set("repository", "id", bin_to_hex(os.urandom(32)))
         self.save_config(path, config)
         self.save_config(path, config)
@@ -385,8 +380,6 @@ class LegacyRepository:
 
 
     def destroy(self):
     def destroy(self):
         """Destroy the repository at `self.path`"""
         """Destroy the repository at `self.path`"""
-        if self.append_only:
-            raise ValueError(self.path + " is in append-only mode")
         self.close()
         self.close()
         os.remove(os.path.join(self.path, "config"))  # kill config first
         os.remove(os.path.join(self.path, "config"))  # kill config first
         shutil.rmtree(self.path)
         shutil.rmtree(self.path)
@@ -468,9 +461,6 @@ class LegacyRepository:
             raise self.InvalidRepositoryConfig(path, "max_segment_size >= %d" % MAX_SEGMENT_SIZE_LIMIT)  # issue 3592
             raise self.InvalidRepositoryConfig(path, "max_segment_size >= %d" % MAX_SEGMENT_SIZE_LIMIT)  # issue 3592
         self.segments_per_dir = self.config.getint("repository", "segments_per_dir")
         self.segments_per_dir = self.config.getint("repository", "segments_per_dir")
         self.additional_free_space = parse_file_size(self.config.get("repository", "additional_free_space", fallback=0))
         self.additional_free_space = parse_file_size(self.config.get("repository", "additional_free_space", fallback=0))
-        # append_only can be set in the constructor
-        # it shouldn't be overridden (True -> False) here
-        self.append_only = self.append_only or self.config.getboolean("repository", "append_only", fallback=False)
         self.id = hex_to_bin(self.config.get("repository", "id").strip(), length=32)
         self.id = hex_to_bin(self.config.get("repository", "id").strip(), length=32)
         self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir)
         self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir)
 
 
@@ -484,7 +474,7 @@ class LegacyRepository:
 
 
     def info(self):
     def info(self):
         """return some infos about the repo (must be opened first)"""
         """return some infos about the repo (must be opened first)"""
-        info = dict(id=self.id, version=self.version, append_only=self.append_only)
+        info = dict(id=self.id, version=self.version)
         self._load_hints()
         self._load_hints()
         return info
         return info
 
 
@@ -506,7 +496,7 @@ class LegacyRepository:
         segment = self.io.write_commit()
         segment = self.io.write_commit()
         self.segments.setdefault(segment, 0)
         self.segments.setdefault(segment, 0)
         self.compact[segment] += LoggedIO.header_fmt.size
         self.compact[segment] += LoggedIO.header_fmt.size
-        if compact and not self.append_only:
+        if compact:
             self.compact_segments(threshold)
             self.compact_segments(threshold)
         self.write_index()
         self.write_index()
         self.rollback()
         self.rollback()
@@ -632,15 +622,6 @@ class LegacyRepository:
         transaction_id = self.io.get_segments_transaction_id()
         transaction_id = self.io.get_segments_transaction_id()
         assert transaction_id is not None
         assert transaction_id is not None
 
 
-        # Log transaction in append-only mode
-        if self.append_only:
-            with open(os.path.join(self.path, "transactions"), "a") as log:
-                print(
-                    "transaction %d, UTC time %s"
-                    % (transaction_id, datetime.now(tz=timezone.utc).isoformat(timespec="microseconds")),
-                    file=log,
-                )
-
         # Write hints file
         # Write hints file
         hints_name = "hints.%d" % transaction_id
         hints_name = "hints.%d" % transaction_id
         hints_file = os.path.join(self.path, hints_name)
         hints_file = os.path.join(self.path, hints_name)
@@ -704,7 +685,7 @@ class LegacyRepository:
         required_free_space += hints_size
         required_free_space += hints_size
 
 
         required_free_space += self.additional_free_space
         required_free_space += self.additional_free_space
-        if not self.append_only:
+        if True:
             full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE
             full_segment_size = self.max_segment_size + MAX_OBJECT_SIZE
             if len(self.compact) < 10:
             if len(self.compact) < 10:
                 # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying
                 # This is mostly for the test suite to avoid overestimated free space needs. This can be annoying
@@ -723,7 +704,7 @@ class LegacyRepository:
                 )
                 )
                 required_free_space += compact_working_space
                 required_free_space += compact_working_space
             else:
             else:
-                # Keep one full worst-case segment free in non-append-only mode
+                # Keep one full worst-case segment free.
                 required_free_space += full_segment_size
                 required_free_space += full_segment_size
 
 
         try:
         try:
@@ -1002,8 +983,6 @@ class LegacyRepository:
         This method verifies all segment checksums and makes sure
         This method verifies all segment checksums and makes sure
         the index is consistent with the data stored in the segments.
         the index is consistent with the data stored in the segments.
         """
         """
-        if self.append_only and repair:
-            raise ValueError(self.path + " is in append-only mode")
         error_found = False
         error_found = False
 
 
         def report_error(msg, *args):
         def report_error(msg, *args):

+ 6 - 24
src/borg/remote.py

@@ -183,7 +183,7 @@ class RepositoryServer:  # pragma: no cover
         "store_move",
         "store_move",
     )
     )
 
 
-    def __init__(self, restrict_to_paths, restrict_to_repositories, append_only, use_socket):
+    def __init__(self, restrict_to_paths, restrict_to_repositories, use_socket):
         self.repository = None
         self.repository = None
         self.RepoCls = None
         self.RepoCls = None
         self.rpc_methods = ("open", "close", "negotiate")
         self.rpc_methods = ("open", "close", "negotiate")
@@ -193,7 +193,6 @@ class RepositoryServer:  # pragma: no cover
         # i.e. it reflects local system policy and generally ranks higher than
         # i.e. it reflects local system policy and generally ranks higher than
         # whatever the client wants, except when initializing a new repository
         # whatever the client wants, except when initializing a new repository
         # (see RepositoryServer.open below).
         # (see RepositoryServer.open below).
-        self.append_only = append_only
         self.client_version = None  # we update this after client sends version information
         self.client_version = None  # we update this after client sends version information
         if use_socket is False:
         if use_socket is False:
             self.socket_path = None
             self.socket_path = None
@@ -371,7 +370,7 @@ class RepositoryServer:  # pragma: no cover
         path = os.path.realpath(path)
         path = os.path.realpath(path)
         return path
         return path
 
 
-    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False, v1_or_v2=False):
+    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, v1_or_v2=False):
         self.RepoCls = LegacyRepository if v1_or_v2 else Repository
         self.RepoCls = LegacyRepository if v1_or_v2 else Repository
         self.rpc_methods = self._legacy_rpc_methods if v1_or_v2 else self._rpc_methods
         self.rpc_methods = self._legacy_rpc_methods if v1_or_v2 else self._rpc_methods
         logging.debug("Resolving repository path %r", path)
         logging.debug("Resolving repository path %r", path)
@@ -395,18 +394,8 @@ class RepositoryServer:  # pragma: no cover
                     break
                     break
             else:
             else:
                 raise PathNotAllowed(path)
                 raise PathNotAllowed(path)
-        # "borg init" on "borg serve --append-only" (=self.append_only) does not create an append only repo,
-        # while "borg init --append-only" (=append_only) does, regardless of the --append-only (self.append_only)
-        # flag for serve.
-        append_only = (not create and self.append_only) or append_only
         self.repository = self.RepoCls(
         self.repository = self.RepoCls(
-            path,
-            create,
-            lock_wait=lock_wait,
-            lock=lock,
-            append_only=append_only,
-            exclusive=exclusive,
-            send_log_cb=self.send_queued_log,
+            path, create, lock_wait=lock_wait, lock=lock, exclusive=exclusive, send_log_cb=self.send_queued_log
         )
         )
         self.repository.__enter__()  # clean exit handled by serve() method
         self.repository.__enter__()  # clean exit handled by serve() method
         return self.repository.id
         return self.repository.id
@@ -574,7 +563,7 @@ class RemoteRepository:
         def required_version(self):
         def required_version(self):
             return self.args[1]
             return self.args[1]
 
 
-    def __init__(self, location, create=False, exclusive=False, lock_wait=1.0, lock=True, append_only=False, args=None):
+    def __init__(self, location, create=False, exclusive=False, lock_wait=1.0, lock=True, args=None):
         self.location = self._location = location
         self.location = self._location = location
         self.preload_ids = []
         self.preload_ids = []
         self.msgid = 0
         self.msgid = 0
@@ -651,16 +640,10 @@ class RemoteRepository:
                 raise Exception("Server insisted on using unsupported protocol version %s" % version)
                 raise Exception("Server insisted on using unsupported protocol version %s" % version)
 
 
             self.id = self.open(
             self.id = self.open(
-                path=self.location.path,
-                create=create,
-                lock_wait=lock_wait,
-                lock=lock,
-                exclusive=exclusive,
-                append_only=append_only,
+                path=self.location.path, create=create, lock_wait=lock_wait, lock=lock, exclusive=exclusive
             )
             )
             info = self.info()
             info = self.info()
             self.version = info["version"]
             self.version = info["version"]
-            self.append_only = info["append_only"]
 
 
         except Exception:
         except Exception:
             self.close()
             self.close()
@@ -965,10 +948,9 @@ class RemoteRepository:
 
 
     @api(
     @api(
         since=parse_version("1.0.0"),
         since=parse_version("1.0.0"),
-        append_only={"since": parse_version("1.0.7"), "previously": False},
         v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True},  # TODO fix version
         v1_or_v2={"since": parse_version("2.0.0b8"), "previously": True},  # TODO fix version
     )
     )
-    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, append_only=False, v1_or_v2=False):
+    def open(self, path, create=False, lock_wait=None, lock=True, exclusive=False, v1_or_v2=False):
         """actual remoting is done via self.call in the @api decorator"""
         """actual remoting is done via self.call in the @api decorator"""
 
 
     @api(since=parse_version("2.0.0a3"))
     @api(since=parse_version("2.0.0a3"))

+ 2 - 12
src/borg/repository.py

@@ -93,16 +93,7 @@ class Repository:
 
 
         exit_mcode = 21
         exit_mcode = 21
 
 
-    def __init__(
-        self,
-        path_or_location,
-        create=False,
-        exclusive=False,
-        lock_wait=1.0,
-        lock=True,
-        append_only=False,
-        send_log_cb=None,
-    ):
+    def __init__(self, path_or_location, create=False, exclusive=False, lock_wait=1.0, lock=True, send_log_cb=None):
         if isinstance(path_or_location, Location):
         if isinstance(path_or_location, Location):
             location = path_or_location
             location = path_or_location
             if location.proto == "file":
             if location.proto == "file":
@@ -139,7 +130,6 @@ class Repository:
         self.created = False
         self.created = False
         self.acceptable_repo_versions = (3,)
         self.acceptable_repo_versions = (3,)
         self.opened = False
         self.opened = False
-        self.append_only = append_only  # XXX not implemented / not implementable
         self.lock = None
         self.lock = None
         self.do_lock = lock
         self.do_lock = lock
         self.lock_wait = lock_wait
         self.lock_wait = lock_wait
@@ -254,7 +244,7 @@ class Repository:
         """return some infos about the repo (must be opened first)"""
         """return some infos about the repo (must be opened first)"""
         # note: don't do anything expensive here or separate the lock refresh into a separate method.
         # note: don't do anything expensive here or separate the lock refresh into a separate method.
         self._lock_refresh()  # do not remove, see do_with_lock()
         self._lock_refresh()  # do not remove, see do_with_lock()
-        info = dict(id=self.id, version=self.version, append_only=self.append_only)
+        info = dict(id=self.id, version=self.version)
         return info
         return info
 
 
     def check(self, repair=False, max_duration=0):
     def check(self, repair=False, max_duration=0):

+ 5 - 5
src/borg/testsuite/archiver/argparsing_test.py

@@ -126,7 +126,7 @@ class TestCommonOptions:
             formatter_class=argparse.RawDescriptionHelpFormatter,
             formatter_class=argparse.RawDescriptionHelpFormatter,
         )
         )
         subparser.set_defaults(func=1234)
         subparser.set_defaults(func=1234)
-        subparser.add_argument("--append-only", dest="append_only", action="store_true")
+        subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true")
 
 
         def parse_vars_from_line(*line):
         def parse_vars_from_line(*line):
             print(line)
             print(line)
@@ -149,19 +149,19 @@ class TestCommonOptions:
             "lock_wait": 1,
             "lock_wait": 1,
             "log_level": "critical",
             "log_level": "critical",
             "progress": False,
             "progress": False,
-            "append_only": False,
+            "foo_bar": False,
             "func": 1234,
             "func": 1234,
         }
         }
 
 
         with pytest.raises(SystemExit):
         with pytest.raises(SystemExit):
-            parse_vars_from_line("--append-only", "subcommand")
+            parse_vars_from_line("--foo-bar", "subcommand")
 
 
         assert parse_vars_from_line("--append=foo", "--append", "bar", "subcommand", "--append", "baz") == {
         assert parse_vars_from_line("--append=foo", "--append", "bar", "subcommand", "--append", "baz") == {
             "append": ["foo", "bar", "baz"],
             "append": ["foo", "bar", "baz"],
             "lock_wait": 1,
             "lock_wait": 1,
             "log_level": "warning",
             "log_level": "warning",
             "progress": False,
             "progress": False,
-            "append_only": False,
+            "foo_bar": False,
             "func": 1234,
             "func": 1234,
         }
         }
 
 
@@ -180,7 +180,7 @@ class TestCommonOptions:
             "lock_wait": 1,
             "lock_wait": 1,
             "log_level": "warning",
             "log_level": "warning",
             "progress": False,
             "progress": False,
-            "append_only": False,
+            "foo_bar": False,
             "func": 1234,
             "func": 1234,
             args_key: args_value,
             args_key: args_value,
         }
         }

+ 0 - 36
src/borg/testsuite/legacyrepository_test.py

@@ -511,39 +511,6 @@ def test_shadow_index_rollback(repository):
         assert repository.shadow_index[H(1)] == []  # because the deletion is considered unstable
         assert repository.shadow_index[H(1)] == []  # because the deletion is considered unstable
 
 
 
 
-def test_destroy_append_only(repository):
-    with repository:
-        # can't destroy append only repo (via the API)
-        repository.append_only = True
-        with pytest.raises(ValueError):
-            repository.destroy()
-        assert repository.append_only
-
-
-def test_append_only(repository):
-    def segments_in_repository(repo):
-        return len(list(repo.io.segment_iterator()))
-
-    with repository:
-        repository.append_only = True
-        repository.put(H(0), fchunk(b"foo"))
-        repository.commit(compact=False)
-
-        repository.append_only = False
-        assert segments_in_repository(repository) == 2
-        repository.put(H(0), fchunk(b"foo"))
-        repository.commit(compact=True)
-        # normal: compact squashes the data together, only one segment
-        assert segments_in_repository(repository) == 2
-
-        repository.append_only = True
-        assert segments_in_repository(repository) == 2
-        repository.put(H(0), fchunk(b"foo"))
-        repository.commit(compact=False)
-        # append only: does not compact, only new segments written
-        assert segments_in_repository(repository) == 4
-
-
 def test_additional_free_space(repository):
 def test_additional_free_space(repository):
     with repository:
     with repository:
         add_keys(repository)
         add_keys(repository)
@@ -680,7 +647,6 @@ def test_unknown_integrity_version(repository):
 
 
 def _subtly_corrupted_hints_setup(repository):
 def _subtly_corrupted_hints_setup(repository):
     with repository:
     with repository:
-        repository.append_only = True
         assert len(repository) == 1
         assert len(repository) == 1
         assert pdchunk(repository.get(H(0))) == b"foo"
         assert pdchunk(repository.get(H(0))) == b"foo"
         repository.put(H(1), fchunk(b"bar"))
         repository.put(H(1), fchunk(b"bar"))
@@ -703,7 +669,6 @@ def test_subtly_corrupted_hints(repository):
     make_auxiliary(repository)
     make_auxiliary(repository)
     _subtly_corrupted_hints_setup(repository)
     _subtly_corrupted_hints_setup(repository)
     with repository:
     with repository:
-        repository.append_only = False
         repository.put(H(3), fchunk(b"1234"))
         repository.put(H(3), fchunk(b"1234"))
         # do a compaction run, which succeeds since the failed checksum prompted a rebuild of the index+hints.
         # do a compaction run, which succeeds since the failed checksum prompted a rebuild of the index+hints.
         repository.commit(compact=True)
         repository.commit(compact=True)
@@ -719,7 +684,6 @@ def test_subtly_corrupted_hints_without_integrity(repository):
     integrity_path = os.path.join(repository.path, "integrity.5")
     integrity_path = os.path.join(repository.path, "integrity.5")
     os.unlink(integrity_path)
     os.unlink(integrity_path)
     with repository:
     with repository:
-        repository.append_only = False
         repository.put(H(3), fchunk(b"1234"))
         repository.put(H(3), fchunk(b"1234"))
         # do a compaction run, which fails since the corrupted refcount wasn't detected and causes an assertion failure.
         # do a compaction run, which fails since the corrupted refcount wasn't detected and causes an assertion failure.
         with pytest.raises(AssertionError) as exc_info:
         with pytest.raises(AssertionError) as exc_info: