borg icon indicating copy to clipboard operation
borg copied to clipboard

Should `borg-2` error on `repo-delete` if the top-level can't be deleted?

Open cr1901 opened this issue 2 months ago • 2 comments

Consider the following directory layout, where I own a directory wjones-2, but cannot delete/create files/directories in its parent:

william@xubuntu-dtrain:/shares/borg/v2-testing$ ls -ld . wjones-2
drwxr-xr-x 5 root    root    5 Oct  3 16:53 .
drwxr-xr-x 2 william william 2 Oct  3 16:54 wjones-2

As of commit 0ee5d6f01, I can create a repository just fine:

william@xubuntu-dtrain:/shares/borg/v2-testing$ borg-2 repo-create -r wjones-2 -e none

Reserve some repository storage space now for emergencies like 'disk full'
by running:
    borg repo-space --reserve 1G

OTOH, I cannot delete said repository without the final rmdir for the top-level repo dir failing, due to not having permissions in the parent:

william@xubuntu-dtrain:/shares/borg/v2-testing$ borg-2 repo-delete -r wjones-2
You requested to DELETE the following repository completely *including* 0 archives it contains:
------------------------------------------------------------------------------
Repository ID: 850879baec49dd84eeabefcf4df5979eb4f5e4c9e6aa14cbceab56f25ec8fd82
Location: /shares/borg/v2-testing/wjones-2
------------------------------------------------------------------------------
Type 'YES' if you understand this and want to continue: YES
Local Exception

Error:

PermissionError: [Errno 13] Permission denied: '/shares/borg/v2-testing/wjones-2'

If reporting bugs, please include the following:

Traceback (most recent call last):
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borg/archiver/__init__.py", line 647, in main
    exit_code = archiver.run(args)
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borg/archiver/__init__.py", line 541, in run
    rc = func(args)
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borg/archiver/_common.py", line 145, in wrapper
    return method(self, args, repository=repository, **kwargs)
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borg/archiver/repo_delete_cmd.py", line 77, in do_repo_delete
    repository.destroy()
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borg/repository.py", line 247, in destroy
    self.store.destroy()
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borgstore/store.py", line 119, in destroy
    self.backend.destroy()
  File "/home/william/.local/pipx/venvs/borgbackup-2/lib/python3.10/site-packages/borgstore/backends/posixfs.py", line 115, in destroy
    shutil.rmtree(os.fspath(self.base_path))
  File "/usr/lib/python3.10/shutil.py", line 731, in rmtree
    onerror(os.rmdir, path, sys.exc_info())
  File "/usr/lib/python3.10/shutil.py", line 729, in rmtree
    os.rmdir(path)
PermissionError: [Errno 13] Permission denied: '/shares/borg/v2-testing/wjones-2'

Platform: Linux xubuntu-dtrain 5.15.0-134-generic #145-Ubuntu SMP Wed Feb 12 20:08:39 UTC 2025 x86_64
Linux: Unknown Linux
Borg: 2.0.0b16.dev329+g0ee5d6f01.d20250928  Python: CPython 3.10.12 msgpack: 1.1.1 fuse: pyfuse3 3.2.0 [pyfuse3,llfuse]
PID: 3508980  CWD: /shares/borg/v2-testing
sys.argv: ['/home/william/.local/bin/borg-2', 'repo-delete', '-r', 'wjones-2']
SSH_ORIGINAL_COMMAND: None

My question is: is this intentional? Since we can successfully create a repo in an existing directory, shouldn't deleting everything up to but not including the top-level be a harmless exception (caught by an except block, and not returning an error)?

(This also applies to borg v1.4.1 and can prob be backported.)

cr1901 avatar Oct 03 '25 21:10 cr1901

For borg2, this issue is a borgstore issue.

The idea is that borg creates the repo starting from nothing and also deletes the repo ending up with nothing. Both of that would require write permissions to the parent directory and that always should be doable by using a parent directory with appropriate permissions.

Additionally, for repo-create, it is also acceptable if the directory already exists or if parents plus that directory can be created. From borgstore:

self.base_path.mkdir(exist_ok=True, parents=True)

That's provided because there is nothing that speaks against doing so (AFAIK).

OTOH, for repo-delete, borgstore just uses:

shutil.rmtree(os.fspath(self.base_path))

The only interesting parameter there is ignore_errors, which would ignore all errors if we used that. There is no "ignore error, but only for the topmost directory".

Update: maybe we could implement it with rmtree's onexc parameter:

If onexc is provided, it must be a callable that accepts three parameters: function, path, and excinfo.

The first parameter, function, is the function which raised the exception; it depends on the platform and implementation. The second parameter, path, will be the path name passed to function. The third parameter, excinfo, is the exception that was raised. Exceptions raised by onexc will not be caught.

https://docs.python.org/3/library/shutil.html#shutil.rmtree

ThomasWaldmann avatar Oct 03 '25 21:10 ThomasWaldmann

I've never used onexc, but my idea was "let rmtree run, detect whether there was an exception, if there was an exception and self.base_path is empty, then chances are the only issue is that its parent doesn't have the correct permissions".

that always should be doable by using a parent directory with appropriate permissions.

Well, yes. You can always nest one level deeper in a directory you own. Just not my preferred way of doing things :P. You'll likely run into parent permissions issues whenever you want your borg repo to be in a directory underneath a mountpoint (since typically only root can mount).

cr1901 avatar Oct 03 '25 21:10 cr1901