Compare commits

...

32 Commits

Author SHA1 Message Date
rolandu e312966a1d
Merge 578fbe087e into 0c1df415d7 2024-04-11 08:45:04 +02:00
Vladimir Malinovskii 0c1df415d7
changed insufficiently reserved length for log message (#8152)
changed log message reserved length
2024-04-06 20:24:10 +02:00
TW 411c763fb8
Merge pull request #8182 from ThomasWaldmann/fix-test-ht-master
format_timedelta: use 3 decimal digits (ms)
2024-04-04 13:31:58 +02:00
Thomas Waldmann 54a85bf56d
format_timedelta: use 3 decimal digits (ms)
maybe this fixes the frequently failing test.
also, giving ms makes more sense than 10ms granularity.
2024-04-04 12:45:28 +02:00
TW 4d2eb0cb1b
Merge pull request #8181 from ThomasWaldmann/github-actions-update-master
update github actions
2024-04-03 19:33:35 +02:00
Thomas Waldmann d893b899fc
update github actions
(avoid deprecation warnings)
2024-04-03 18:26:35 +02:00
TW fb4b4cfeb8
Merge pull request #8180 from ThomasWaldmann/update-requirements-master
update development.lock.txt
2024-04-03 17:52:24 +02:00
Thomas Waldmann bb50246bc4
update development.lock.txt 2024-04-03 17:13:18 +02:00
TW c5abfe1ee9
Merge pull request #8178 from ThomasWaldmann/acl-error-handling-master
improve acl_get / acl_set error handling (master)
2024-04-03 17:02:14 +02:00
Thomas Waldmann 4e5bf28473
Linux: refactor acl_get 2024-04-02 01:38:31 +02:00
Thomas Waldmann 64b7b5fdd4
FreeBSD: check first if kind of ACL can be set on a file 2024-04-02 01:38:30 +02:00
Thomas Waldmann 4ebb5cdf3c
FreeBSD: simplify numeric_ids check 2024-04-02 01:38:28 +02:00
Thomas Waldmann 7df170c946
FreeBSD: added tests, only get default ACL from dirs 2024-04-02 01:38:27 +02:00
Thomas Waldmann d3694271eb
FreeBSD: acl_get: raise OSError if lpathconf fails
Previously:
- acl_get just returned for lpathconf returning EINVAL
- acl_get silently ignored all other lpathconf errors and
  implied it is not a NFS4 acl

Now:
- not sure why the EINVAL silent return was done, but it seems
  wrong. guess it could be the system not implementing a check
  for nfs4. but in that case guess we still would like to get
  the default and access ACL!? Thus, I removed the silent return.
- raise OSError for all lpathconf errors

Cosmetic: add a nfs4_acl boolean, so the code reads better.
2024-04-02 01:38:26 +02:00
Thomas Waldmann 30f4518058
FreeBSD: acl_get: add an acl_extended_* call
... to implement same semantics as on linux (only store ACL
if it defines permissions other than those defined by the
traditional file permissions).

Looks like there is no call working with an fd on FreeBSD.
2024-04-02 01:38:24 +02:00
Thomas Waldmann 4cc4516c59
Linux: acl_set bug fix: always fsencode path
We use path when raising OSErrors, even if we have an fd.
2024-04-02 01:38:23 +02:00
Thomas Waldmann 96cac5f381
Linux: acl_get: use "nofollow" variant of acl_extended_file call
This is NOT a bug fix, because the previous code contained a
check for symlinks before that line - because symlinks can not
have ACLs under Linux.

Now, this "is it a symlink" check is removed to simplify the
code and the "nofollow" variant of acl_extended_file* is used
to look at the symlink fs object (in the symlink case).

It then should tell us that this does NOT have an extended ACL
(because symlinks can't have ACLs) and so we return there.

Overall the code gets simpler and looks less suspect.
2024-04-02 01:38:21 +02:00
Thomas Waldmann beac2fa9ae
Linux: acl_get: raise OSError for errors in acl_extended_* call
Previously, these conditions were handled the same (just return):
- no extended acl here
- some error happened (e.g. ACLs unsupported, bad file descriptor, file not found, permission error, ...)

Now there will be OSErrors for the error cases.
2024-04-02 01:38:20 +02:00
Thomas Waldmann 1269c852bf
create/extract: ignore OSError if ACLs are not supported (ENOTSUP)
but do not silence other OSErrors.
2024-04-02 01:38:18 +02:00
Thomas Waldmann bafea3b5de
platform tests: misc. minor cleanups
- remove unused global / import
- use is_linux and is_darwin
- rename darwin acl test method
2024-04-02 01:38:17 +02:00
Thomas Waldmann d5396feebd
improve are_acls_working function
- ACLs are not working, if ENOTSUP ("Operation not supported") happens
- fix check for macOS
  On macOS borg uses "acl_extended", not "acl_access" and
  also the ACL text format is a bit different.
2024-04-02 01:38:15 +02:00
Thomas Waldmann b3554cdc0f
raise OSError if acl_to_text / acl_from_text returns NULL
Also did a small structural refactors there.
2024-04-02 01:38:14 +02:00
Thomas Waldmann a75945ed0d
improve acl_get / acl_set error handling, see #4049 2024-04-02 01:38:12 +02:00
TW 7f15c14fc6
Merge pull request #8176 from ThomasWaldmann/vagrant-updates-master
Vagrant updates (master)
2024-04-01 22:17:26 +02:00
TW 7dd0403bd3
Merge pull request #8177 from ThomasWaldmann/debounce-sigint-master
fix Ctrl-C / SIGINT behaviour for pyinstaller-made binaries, fixes #8155 (master)
2024-04-01 22:17:10 +02:00
Thomas Waldmann c157db739b
fix Ctrl-C / SIGINT behaviour for pyinstaller-made binaries, fixes #8155 2024-04-01 20:41:47 +02:00
Thomas Waldmann f28084d773
vagrant: ubuntu lunar -> noble VM
Noble should become stable / LTS soon.
2024-04-01 20:36:06 +02:00
Thomas Waldmann b25caafc94
vagrant: remove buster VM
It's already outdated now and its libxxhash
does not support pkg-config discovery.
2024-04-01 20:32:24 +02:00
Thomas Waldmann ffc1e3ef6f
vagrant: use pyinstaller 6.5.0 2024-04-01 20:28:56 +02:00
Thomas Waldmann 7ac7c79563
use python 3.11.8 for binary builds 2024-04-01 20:26:17 +02:00
Thomas Waldmann 4ff0ba48f8
vagrant: openindiana updates 2024-04-01 20:22:30 +02:00
rolandu 578fbe087e
Clarification of keyfile backup
Proposed clarification in response to my confusion in https://github.com/borgbackup/borg/issues/6204.
2022-01-30 20:26:12 +00:00
17 changed files with 226 additions and 160 deletions

View File

@ -9,7 +9,7 @@ jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
version: "~= 23.0"

View File

@ -78,11 +78,11 @@ jobs:
# just fetching 1 commit is not enough for setuptools-scm, so we fetch all
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.d/development.txt') }}
@ -114,7 +114,7 @@ jobs:
#sudo -E bash -c "tox -e py"
tox --skip-missing-interpreters
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
OS: ${{ runner.os }}
python: ${{ matrix.python-version }}

44
Vagrantfile vendored
View File

@ -28,8 +28,6 @@ def packages_debianoid(user)
apt install -y python3-dev python3-setuptools virtualenv
# for building python:
apt install -y zlib1g-dev libbz2-dev libncurses5-dev libreadline-dev liblzma-dev libsqlite3-dev libffi-dev
# older debian / ubuntu have no .pc file for these, so we need to point at the lib/header location:
echo 'export BORG_LIBXXHASH_PREFIX=/usr' >> ~vagrant/.bash_profile
EOF
end
@ -132,11 +130,13 @@ def packages_openindiana
return <<-EOF
# needs separate provisioning step + reboot:
#pkg update
#pkg install gcc-7 python-39 setuptools-39
pkg install gcc-13 git pkg-config libxxhash
ln -sf /usr/bin/python3.9 /usr/bin/python3
python3 -m ensurepip
ln -sf /usr/bin/pip3.9 /usr/bin/pip3
pip3 install virtualenv
# let borg's pkg-config find openssl:
pfexec pkg set-mediator -V 3.1 openssl
EOF
end
@ -166,7 +166,7 @@ def install_pythons(boxname)
. ~/.bash_profile
echo "PYTHON_CONFIGURE_OPTS: ${PYTHON_CONFIGURE_OPTS}"
pyenv install 3.12.0 # tests
pyenv install 3.11.7 # tests, binary build
pyenv install 3.11.8 # tests, binary build
pyenv install 3.10.2 # tests
pyenv install 3.9.4 # tests
pyenv rehash
@ -186,8 +186,8 @@ def build_pyenv_venv(boxname)
. ~/.bash_profile
cd /vagrant/borg
# use the latest 3.11 release
pyenv global 3.11.7
pyenv virtualenv 3.11.7 borg-env
pyenv global 3.11.8
pyenv virtualenv 3.11.8 borg-env
ln -s ~/.pyenv/versions/borg-env .
EOF
end
@ -210,7 +210,7 @@ def install_pyinstaller()
. ~/.bash_profile
cd /vagrant/borg
. borg-env/bin/activate
pip install 'pyinstaller==6.3.0'
pip install 'pyinstaller==6.5.0'
EOF
end
@ -233,8 +233,8 @@ def run_tests(boxname, skip_env)
. ../borg-env/bin/activate
if which pyenv 2> /dev/null; then
# for testing, use the earliest point releases of the supported python versions:
pyenv global 3.9.4 3.10.2 3.11.7 3.12.0
pyenv local 3.9.4 3.10.2 3.11.7 3.12.0
pyenv global 3.9.4 3.10.2 3.11.8 3.12.0
pyenv local 3.9.4 3.10.2 3.11.8 3.12.0
fi
# otherwise: just use the system python
# some OSes can only run specific test envs, e.g. because they miss FUSE support:
@ -275,16 +275,16 @@ Vagrant.configure(2) do |config|
v.cpus = $cpus
end
config.vm.define "lunar64" do |b|
b.vm.box = "ubuntu/lunar64"
config.vm.define "noble64" do |b|
b.vm.box = "ubuntu/noble64"
b.vm.provider :virtualbox do |v|
v.memory = 1024 + $wmem
end
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("lunar64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_sys_venv("noble64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("lunar64", ".*none.*")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("noble64", ".*none.*")
end
config.vm.define "jammy64" do |b|
@ -331,22 +331,6 @@ Vagrant.configure(2) do |config|
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("bullseye64", ".*none.*")
end
config.vm.define "buster64" do |b|
b.vm.box = "debian/buster64"
b.vm.provider :virtualbox do |v|
v.memory = 1024 + $wmem
end
b.vm.provision "fs init", :type => :shell, :inline => fs_init("vagrant")
b.vm.provision "packages debianoid", :type => :shell, :inline => packages_debianoid("vagrant")
b.vm.provision "install pyenv", :type => :shell, :privileged => false, :inline => install_pyenv("buster64")
b.vm.provision "install pythons", :type => :shell, :privileged => false, :inline => install_pythons("buster64")
b.vm.provision "build env", :type => :shell, :privileged => false, :inline => build_pyenv_venv("buster64")
b.vm.provision "install borg", :type => :shell, :privileged => false, :inline => install_borg("llfuse")
b.vm.provision "install pyinstaller", :type => :shell, :privileged => false, :inline => install_pyinstaller()
b.vm.provision "build binary with pyinstaller", :type => :shell, :privileged => false, :inline => build_binary_with_pyinstaller("buster64")
b.vm.provision "run tests", :type => :shell, :privileged => false, :inline => run_tests("buster64", ".*none.*")
end
config.vm.define "freebsd64" do |b|
b.vm.box = "generic/freebsd14"
b.vm.provider :virtualbox do |v|
@ -417,7 +401,7 @@ Vagrant.configure(2) do |config|
# rsync on openindiana has troubles, does not set correct owner for /vagrant/borg and thus gives lots of
# permission errors. can be manually fixed in the VM by: sudo chown -R vagrant /vagrant/borg ; then rsync again.
config.vm.define "openindiana64" do |b|
b.vm.box = "openindiana"
b.vm.box = "openindiana/hipster"
b.vm.provider :virtualbox do |v|
v.memory = 2048 + $wmem
end

View File

@ -414,10 +414,12 @@ How important is the $HOME/.config/borg directory?
The Borg config directory has content that you should take care of:
``keys`` subdirectory
All your borg keyfile keys are stored in this directory. Please note that
borg repokey keys are stored inside the repository. You MUST make sure to have an
independent backup of these keyfiles, otherwise you cannot access your backups anymore if you lose
them. You also MUST keep these files secret; everyone who gains access to your repository and has
All your borg keyfile keys are stored in this directory. Please note that borg repokey keys are stored inside the repository.
If you use keyfile encryption, you MUST make sure to have an independent backup of these keyfiles, otherwise you cannot access your backups anymore if you lose
them. If you use repokey encryption it is also highly recommended that you backup the keyfiles in case the originals (stored within the repository) get damaged. The best way to create a backup is using the ``borg key export`` command.
You also MUST keep these files secret; everyone who gains access to your repository and has
the corresponding keyfile (and the key passphrase) can extract it.
Make sure that only you have access to the Borg config directory.

View File

@ -1,14 +1,14 @@
setuptools==68.2.2
setuptools==69.2.0
setuptools-scm==8.0.4
pip==23.3.2
wheel==0.41.3
virtualenv==20.25.0
build==1.0.3
pip==24.0
wheel==0.43.0
virtualenv==20.25.1
build==1.2.1
pkgconfig==1.5.5
tox==4.11.3
pytest==7.4.3
pytest-xdist==3.3.1
pytest-cov==4.1.0
tox==4.14.2
pytest==8.1.1
pytest-xdist==3.5.0
pytest-cov==5.0.0
pytest-benchmark==4.0.0
Cython==3.0.5
pre-commit==3.5.0
Cython==3.0.10
pre-commit==3.7.0

View File

@ -964,7 +964,11 @@ Duration: {0.duration}
if not symlink:
os.chmod(path, item.mode)
if not self.noacls:
acl_set(path, item, self.numeric_ids, fd=fd)
try:
acl_set(path, item, self.numeric_ids, fd=fd)
except OSError as e:
if e.errno not in (errno.ENOTSUP,):
raise
if not self.noxattrs and "xattrs" in item:
# chown removes Linux capabilities, so set the extended attributes at the end, after chown,
# since they include the Linux capabilities in the "security.capability" attribute.
@ -1210,7 +1214,11 @@ class MetadataCollector:
attrs["xattrs"] = StableDict(xattrs)
if not self.noacls:
with backup_io("extended stat (ACLs)"):
acl_get(path, attrs, st, self.numeric_ids, fd=fd)
try:
acl_get(path, attrs, st, self.numeric_ids, fd=fd)
except OSError as e:
if e.errno not in (errno.ENOTSUP,):
raise
return attrs
def stat_attrs(self, st, path, fd=None):

View File

@ -169,7 +169,7 @@ class PruneMixIn:
or (args.list_pruned and archive in to_delete)
or (args.list_kept and archive not in to_delete)
):
list_logger.info(f"{log_message:<40} {formatter.format_item(archive, jsonline=False)}")
list_logger.info(f"{log_message:<44} {formatter.format_item(archive, jsonline=False)}")
pi.finish()
if sig_int:
# Ctrl-C / SIGINT: do not checkpoint (commit) again, we already have a checkpoint in this case.

View File

@ -195,6 +195,8 @@ class SigIntManager:
self._action_triggered = False
self._action_done = False
self.ctx = signal_handler("SIGINT", self.handler)
self.debounce_interval = 20000000 # ns
self.last = None # monotonic time when we last processed SIGINT
def __bool__(self):
# this will be True (and stay True) after the first Ctrl-C/SIGINT
@ -215,10 +217,22 @@ class SigIntManager:
self._action_done = True
def handler(self, sig_no, stack):
# handle the first ctrl-c / SIGINT.
self.__exit__(None, None, None)
self._sig_int_triggered = True
self._action_triggered = True
# Ignore a SIGINT if it comes too quickly after the last one, e.g. because it
# was caused by the same Ctrl-C key press and a parent process forwarded it to us.
# This can easily happen for the pyinstaller-made binaries because the bootloader
# process and the borg process are in same process group (see #8155), but maybe also
# under other circumstances.
now = time.monotonic_ns()
if self.last is None: # first SIGINT
self.last = now
self._sig_int_triggered = True
self._action_triggered = True
elif now - self.last >= self.debounce_interval: # second SIGINT
# restore the original signal handler for the 3rd+ SIGINT -
# this implies that this handler here loses control!
self.__exit__(None, None, None)
# handle 2nd SIGINT like the default handler would do it:
raise KeyboardInterrupt # python docs say this might show up at an arbitrary place.
def __enter__(self):
self.ctx.__enter__()

View File

@ -100,7 +100,7 @@ def format_timedelta(td):
s = ts % 60
m = int(ts / 60) % 60
h = int(ts / 3600) % 24
txt = "%.2f seconds" % s
txt = "%.3f seconds" % s
if m:
txt = "%d minutes %s" % (m, txt)
if h:

View File

@ -1,6 +1,7 @@
import os
from libc.stdint cimport uint32_t
from libc cimport errno
from .posix import user2uid, group2gid
from ..helpers import safe_decode, safe_encode
@ -115,20 +116,25 @@ def _remove_non_numeric_identifier(acl):
def acl_get(path, item, st, numeric_ids=False, fd=None):
cdef acl_t acl = NULL
cdef char *text = NULL
if isinstance(path, str):
path = os.fsencode(path)
try:
if fd is not None:
acl = acl_get_fd_np(fd, ACL_TYPE_EXTENDED)
else:
if isinstance(path, str):
path = os.fsencode(path)
acl = acl_get_link_np(path, ACL_TYPE_EXTENDED)
if acl is not NULL:
text = acl_to_text(acl, NULL)
if text is not NULL:
if numeric_ids:
item['acl_extended'] = _remove_non_numeric_identifier(text)
else:
item['acl_extended'] = text
if acl == NULL:
if errno.errno == errno.ENOENT:
# macOS weirdness: if a file has no ACLs, it sets errno to ENOENT. :-(
return
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
text = acl_to_text(acl, NULL)
if text == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if numeric_ids:
item['acl_extended'] = _remove_non_numeric_identifier(text)
else:
item['acl_extended'] = text
finally:
acl_free(text)
acl_free(acl)
@ -139,16 +145,19 @@ def acl_set(path, item, numeric_ids=False, fd=None):
acl_text = item.get('acl_extended')
if acl_text is not None:
try:
if isinstance(path, str):
path = os.fsencode(path)
if numeric_ids:
acl = acl_from_text(acl_text)
else:
acl = acl_from_text(<bytes>_remove_numeric_id_if_possible(acl_text))
if acl is not NULL:
if fd is not None:
acl_set_fd_np(fd, acl, ACL_TYPE_EXTENDED)
else:
if isinstance(path, str):
path = os.fsencode(path)
acl_set_link_np(path, ACL_TYPE_EXTENDED, acl)
if acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if fd is not None:
if acl_set_fd_np(fd, acl, ACL_TYPE_EXTENDED) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
else:
if acl_set_link_np(path, ACL_TYPE_EXTENDED, acl) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
finally:
acl_free(acl)

View File

@ -1,4 +1,7 @@
import os
import stat
from libc cimport errno
from .posix import posix_acl_use_stored_uid_gid
from ..helpers import safe_encode, safe_decode
@ -6,10 +9,6 @@ from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lst
API_VERSION = '1.2_05'
cdef extern from "errno.h":
int errno
int EINVAL
cdef extern from "sys/extattr.h":
ssize_t c_extattr_list_file "extattr_list_file" (const char *path, int attrnamespace, void *data, size_t nbytes)
ssize_t c_extattr_list_link "extattr_list_link" (const char *path, int attrnamespace, void *data, size_t nbytes)
@ -44,10 +43,12 @@ cdef extern from "sys/acl.h":
char *acl_to_text_np(acl_t acl, ssize_t *len, int flags)
int ACL_TEXT_NUMERIC_IDS
int ACL_TEXT_APPEND_ID
int acl_extended_link_np(const char * path) # check also: acl_is_trivial_np
cdef extern from "unistd.h":
long lpathconf(const char *path, int name)
int _PC_ACL_NFS4
int _PC_ACL_EXTENDED
# On FreeBSD, borg currently only deals with the USER namespace as it is unclear
@ -124,21 +125,21 @@ def setxattr(path, name, value, *, follow_symlinks=False):
cdef _get_acl(p, type, item, attribute, flags, fd=None):
cdef acl_t acl = NULL
cdef char *text = NULL
try:
if fd is not None:
acl = acl_get_fd_np(fd, type)
else:
acl = acl_get_link_np(p, type)
if acl is not NULL:
text = acl_to_text_np(acl, NULL, flags)
if text is not NULL:
item[attribute] = text
finally:
acl_free(text)
cdef acl_t acl
cdef char *text
if fd is not None:
acl = acl_get_fd_np(fd, type)
else:
acl = acl_get_link_np(p, type)
if acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(p))
text = acl_to_text_np(acl, NULL, flags)
if text == NULL:
acl_free(acl)
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(p))
item[attribute] = text
acl_free(text)
acl_free(acl)
def acl_get(path, item, st, numeric_ids=False, fd=None):
"""Saves ACL Entries
@ -146,34 +147,46 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
If `numeric_ids` is True the user/group field is not preserved only uid/gid
"""
cdef int flags = ACL_TEXT_APPEND_ID
flags |= ACL_TEXT_NUMERIC_IDS if numeric_ids else 0
if isinstance(path, str):
path = os.fsencode(path)
ret = lpathconf(path, _PC_ACL_NFS4)
if ret < 0 and errno == EINVAL:
ret = acl_extended_link_np(path)
if ret < 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if ret == 0:
# there is no ACL defining permissions other than those defined by the traditional file permission bits.
return
flags |= ACL_TEXT_NUMERIC_IDS if numeric_ids else 0
if ret > 0:
ret = lpathconf(path, _PC_ACL_NFS4)
if ret < 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
nfs4_acl = ret == 1
if nfs4_acl:
_get_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', flags, fd=fd)
else:
_get_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', flags, fd=fd)
_get_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', flags, fd=fd)
if stat.S_ISDIR(st.st_mode):
_get_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', flags, fd=fd)
cdef _set_acl(path, type, item, attribute, numeric_ids=False, fd=None):
cdef acl_t acl = NULL
text = item.get(attribute)
if text is not None:
if numeric_ids and type == ACL_TYPE_NFS4:
text = _nfs4_use_stored_uid_gid(text)
elif numeric_ids and type in (ACL_TYPE_ACCESS, ACL_TYPE_DEFAULT):
text = posix_acl_use_stored_uid_gid(text)
if text:
if numeric_ids:
if type == ACL_TYPE_NFS4:
text = _nfs4_use_stored_uid_gid(text)
elif type in (ACL_TYPE_ACCESS, ACL_TYPE_DEFAULT):
text = posix_acl_use_stored_uid_gid(text)
acl = acl_from_text(<bytes>text)
if acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
try:
acl = acl_from_text(<bytes> text)
if acl is not NULL:
if fd is not None:
acl_set_fd_np(fd, acl, type)
else:
acl_set_link_np(path, type, acl)
if fd is not None:
if acl_set_fd_np(fd, acl, type) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
else:
if acl_set_link_np(path, type, acl) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
finally:
acl_free(acl)
@ -201,6 +214,14 @@ def acl_set(path, item, numeric_ids=False, fd=None):
"""
if isinstance(path, str):
path = os.fsencode(path)
_set_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', numeric_ids, fd=fd)
_set_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', numeric_ids, fd=fd)
_set_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', numeric_ids, fd=fd)
ret = lpathconf(path, _PC_ACL_NFS4)
if ret < 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if ret == 1:
_set_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', numeric_ids, fd=fd)
ret = lpathconf(path, _PC_ACL_EXTENDED)
if ret < 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if ret == 1:
_set_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', numeric_ids, fd=fd)
_set_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', numeric_ids, fd=fd)

View File

@ -50,7 +50,7 @@ cdef extern from "sys/acl.h":
char *acl_to_text(acl_t acl, ssize_t *len)
cdef extern from "acl/libacl.h":
int acl_extended_file(const char *path)
int acl_extended_file_nofollow(const char *path)
int acl_extended_fd(int fd)
cdef extern from "linux/fs.h":
@ -233,15 +233,19 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
cdef acl_t access_acl = NULL
cdef char *default_text = NULL
cdef char *access_text = NULL
cdef int ret = 0
if stat.S_ISLNK(st.st_mode):
# symlinks can not have ACLs
return
if isinstance(path, str):
path = os.fsencode(path)
if (fd is not None and acl_extended_fd(fd) <= 0
or
fd is None and acl_extended_file(path) <= 0):
if fd is not None:
ret = acl_extended_fd(fd)
else:
ret = acl_extended_file_nofollow(path)
if ret < 0:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if ret == 0:
# there is no ACL defining permissions other than those defined by the traditional file permission bits.
# note: this should also be the case for symlink fs objects, as they can not have ACLs.
return
if numeric_ids:
converter = acl_numeric_ids
@ -252,25 +256,28 @@ def acl_get(path, item, st, numeric_ids=False, fd=None):
access_acl = acl_get_fd(fd)
else:
access_acl = acl_get_file(path, ACL_TYPE_ACCESS)
if access_acl is not NULL:
access_text = acl_to_text(access_acl, NULL)
if access_text is not NULL:
item['acl_access'] = converter(access_text)
if access_acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
access_text = acl_to_text(access_acl, NULL)
if access_text == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
item['acl_access'] = converter(access_text)
finally:
acl_free(access_text)
acl_free(access_acl)
try:
if stat.S_ISDIR(st.st_mode):
# only directories can have a default ACL. there is no fd-based api to get it.
if stat.S_ISDIR(st.st_mode):
# only directories can have a default ACL. there is no fd-based api to get it.
try:
default_acl = acl_get_file(path, ACL_TYPE_DEFAULT)
if default_acl is not NULL:
default_text = acl_to_text(default_acl, NULL)
if default_text is not NULL:
item['acl_default'] = converter(default_text)
finally:
acl_free(default_text)
acl_free(default_acl)
if default_acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
default_text = acl_to_text(default_acl, NULL)
if default_text == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
item['acl_default'] = converter(default_text)
finally:
acl_free(default_text)
acl_free(default_acl)
def acl_set(path, item, numeric_ids=False, fd=None):
@ -281,7 +288,7 @@ def acl_set(path, item, numeric_ids=False, fd=None):
# Linux does not support setting ACLs on symlinks
return
if fd is None and isinstance(path, str):
if isinstance(path, str):
path = os.fsencode(path)
if numeric_ids:
converter = posix_acl_use_stored_uid_gid
@ -290,21 +297,26 @@ def acl_set(path, item, numeric_ids=False, fd=None):
access_text = item.get('acl_access')
if access_text is not None:
try:
access_acl = acl_from_text(<bytes> converter(access_text))
if access_acl is not NULL:
if fd is not None:
acl_set_fd(fd, access_acl)
else:
acl_set_file(path, ACL_TYPE_ACCESS, access_acl)
access_acl = acl_from_text(<bytes>converter(access_text))
if access_acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
if fd is not None:
if acl_set_fd(fd, access_acl) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
else:
if acl_set_file(path, ACL_TYPE_ACCESS, access_acl) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
finally:
acl_free(access_acl)
default_text = item.get('acl_default')
if default_text is not None:
try:
default_acl = acl_from_text(<bytes> converter(default_text))
if default_acl is not NULL:
# only directories can get a default ACL. there is no fd-based api to set it.
acl_set_file(path, ACL_TYPE_DEFAULT, default_acl)
default_acl = acl_from_text(<bytes>converter(default_text))
if default_acl == NULL:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
# only directories can get a default ACL. there is no fd-based api to set it.
if acl_set_file(path, ACL_TYPE_DEFAULT, default_acl) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path))
finally:
acl_free(default_acl)

View File

@ -61,8 +61,8 @@ def test_stats_format(stats):
Number of files: 1
Original size: 20 B
Deduplicated size: 20 B
Time spent in hashing: 0.00 seconds
Time spent in chunking: 0.00 seconds
Time spent in hashing: 0.000 seconds
Time spent in chunking: 0.000 seconds
Added files: 0
Unchanged files: 0
Modified files: 0

View File

@ -368,7 +368,7 @@ def test_text_invalid(text):
def test_format_timedelta():
t0 = datetime(2001, 1, 1, 10, 20, 3, 0)
t1 = datetime(2001, 1, 1, 12, 20, 4, 100000)
assert format_timedelta(t1 - t0) == "2 hours 1.10 seconds"
assert format_timedelta(t1 - t0) == "2 hours 1.100 seconds"
@pytest.mark.parametrize(

View File

@ -1,3 +1,4 @@
import errno
import functools
import os
@ -31,25 +32,38 @@ def are_acls_working():
with unopened_tempfile() as filepath:
open(filepath, "w").close()
try:
if is_freebsd:
access = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-\n"
contained = b"user:root:rw-"
if is_darwin:
acl_key = "acl_extended"
acl_value = b"!#acl 1\nuser:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\n"
elif is_linux:
access = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-:0\n"
contained = b"user:root:rw-:0"
elif is_darwin:
return True # improve?
acl_key = "acl_access"
acl_value = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-:9999\ngroup:root:rw-:9999\n"
elif is_freebsd:
acl_key = "acl_access"
acl_value = b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-\ngroup:wheel:rw-\n"
else:
return False # unsupported platform
acl = {"acl_access": access}
acl_set(filepath, acl)
return False # ACLs unsupported on this platform.
write_acl = {acl_key: acl_value}
acl_set(filepath, write_acl)
read_acl = {}
acl_get(filepath, read_acl, os.stat(filepath))
read_acl_access = read_acl.get("acl_access", None)
if read_acl_access and contained in read_acl_access:
return True
acl = read_acl.get(acl_key, None)
if acl is not None:
if is_darwin:
check_for = b"root:0:allow:read"
elif is_linux:
check_for = b"user::rw-"
elif is_freebsd:
check_for = b"user::rw-"
else:
return False # ACLs unsupported on this platform.
if check_for in acl:
return True
except PermissionError:
pass
except OSError as e:
if e.errno not in (errno.ENOTSUP,):
raise
return False

View File

@ -20,7 +20,7 @@ def set_acl(path, acl, numeric_ids=False):
@skipif_acls_not_working
def test_access_acl():
def test_extended_acl():
file = tempfile.NamedTemporaryFile()
assert get_acl(file.name) == {}
set_acl(

View File

@ -49,6 +49,7 @@ def set_acl(path, access=None, default=None, nfs4=None, numeric_ids=False):
@skipif_acls_not_working
def test_access_acl():
file1 = tempfile.NamedTemporaryFile()
assert get_acl(file1.name) == {}
set_acl(
file1.name,
access=b"user::rw-\ngroup::r--\nmask::rw-\nother::---\nuser:root:rw-\ngroup:wheel:rw-\n",
@ -86,6 +87,7 @@ def test_access_acl():
@skipif_acls_not_working
def test_default_acl():
tmpdir = tempfile.mkdtemp()
assert get_acl(tmpdir) == {}
set_acl(tmpdir, access=ACCESS_ACL, default=DEFAULT_ACL)
assert get_acl(tmpdir)["acl_access"] == ACCESS_ACL
assert get_acl(tmpdir)["acl_default"] == DEFAULT_ACL