mirror of https://github.com/borgbackup/borg.git
Merge branch 'master' into move-to-src
This commit is contained in:
commit
3ce35f6843
|
@ -11,6 +11,7 @@ crypto.c
|
||||||
platform_darwin.c
|
platform_darwin.c
|
||||||
platform_freebsd.c
|
platform_freebsd.c
|
||||||
platform_linux.c
|
platform_linux.c
|
||||||
|
platform_posix.c
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
|
@ -202,8 +202,8 @@ def install_borg(boxname)
|
||||||
rm -f borg/{chunker,crypto,compress,hashindex,platform_linux}.c
|
rm -f borg/{chunker,crypto,compress,hashindex,platform_linux}.c
|
||||||
rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__
|
rm -rf borg/__pycache__ borg/support/__pycache__ borg/testsuite/__pycache__
|
||||||
pip install -r requirements.d/development.txt
|
pip install -r requirements.d/development.txt
|
||||||
pip install -r requirements.d/fuse.txt
|
# by using [fuse], setup.py can handle different fuse requirements:
|
||||||
pip install -e .
|
pip install -e .[fuse]
|
||||||
EOF
|
EOF
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# This is a hack to fix path problems because "borg" (the package) is in the source root.
|
||||||
|
# When importing the conftest an "import borg" can incorrectly import the borg from the
|
||||||
|
# source root instead of the one installed in the environment.
|
||||||
|
# The workaround is to remove entries pointing there from the path and check whether "borg"
|
||||||
|
# is still importable. If it is not, then it has not been installed in the environment
|
||||||
|
# and the entries are put back.
|
||||||
|
#
|
||||||
|
# TODO: After moving the package to src/: remove this.
|
||||||
|
|
||||||
|
original_path = list(sys.path)
|
||||||
|
for entry in original_path:
|
||||||
|
if entry == '' or entry.endswith('/borg'):
|
||||||
|
sys.path.remove(entry)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import borg
|
||||||
|
except ImportError:
|
||||||
|
sys.path = original_path
|
||||||
|
|
||||||
|
from borg.logger import setup_logging
|
||||||
|
|
||||||
|
# Ensure that the loggers exist for all tests
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
from borg.testsuite import has_lchflags, no_lchlfags_because, has_llfuse
|
||||||
|
from borg.testsuite.platform import fakeroot_detected
|
||||||
|
from borg import xattr
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config, startdir):
|
||||||
|
yesno = ['no', 'yes']
|
||||||
|
flags = 'Testing BSD-style flags: %s %s' % (yesno[has_lchflags], no_lchlfags_because)
|
||||||
|
fakeroot = 'fakeroot: %s (>=1.20.2: %s)' % (
|
||||||
|
yesno[fakeroot_detected()],
|
||||||
|
yesno[xattr.XATTR_FAKEROOT])
|
||||||
|
llfuse = 'Testing fuse: %s' % yesno[has_llfuse]
|
||||||
|
return '\n'.join((flags, llfuse, fakeroot))
|
72
docs/api.rst
72
docs/api.rst
|
@ -2,31 +2,11 @@
|
||||||
API Documentation
|
API Documentation
|
||||||
=================
|
=================
|
||||||
|
|
||||||
.. automodule:: borg.platform
|
.. automodule:: borg.archiver
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.shellpattern
|
.. automodule:: borg.upgrader
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.locking
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.hash_sizes
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.logger
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.remote
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.fuse
|
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
@ -34,7 +14,23 @@ API Documentation
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.helpers
|
.. automodule:: borg.fuse
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.platform
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.locking
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.shellpattern
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.repository
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
@ -42,15 +38,19 @@ API Documentation
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.remote
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.hash_sizes
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.xattr
|
.. automodule:: borg.xattr
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.archiver
|
.. automodule:: borg.helpers
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.repository
|
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ API Documentation
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.upgrader
|
.. automodule:: borg.logger
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
@ -70,15 +70,15 @@ API Documentation
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.compress
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
.. automodule:: borg.platform_linux
|
.. automodule:: borg.platform_linux
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.crypto
|
.. automodule:: borg.hashindex
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
.. automodule:: borg.compress
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
@ -86,10 +86,10 @@ API Documentation
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.platform_freebsd
|
.. automodule:: borg.crypto
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
.. automodule:: borg.hashindex
|
.. automodule:: borg.platform_freebsd
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
|
@ -6,6 +6,10 @@ Version 1.1.0 (not released yet)
|
||||||
|
|
||||||
New features:
|
New features:
|
||||||
|
|
||||||
|
- borg check: will not produce the "Checking segments" output unless
|
||||||
|
new --progress option is passed, #824.
|
||||||
|
- options that imply output (--show-rc, --show-version, --list, --stats,
|
||||||
|
--progress) don't need -v/--info to have that output displayed, #865
|
||||||
- borg recreate: re-create existing archives, #787 #686 #630 #70, also see
|
- borg recreate: re-create existing archives, #787 #686 #630 #70, also see
|
||||||
#757, #770.
|
#757, #770.
|
||||||
|
|
||||||
|
@ -34,7 +38,7 @@ New features:
|
||||||
- borg comment: add archive comments, #842
|
- borg comment: add archive comments, #842
|
||||||
- provide "borgfs" wrapper for borg mount, enables usage via fstab, #743
|
- provide "borgfs" wrapper for borg mount, enables usage via fstab, #743
|
||||||
- create: add 'x' status for excluded paths, #814
|
- create: add 'x' status for excluded paths, #814
|
||||||
- --show-version: shows/logs the borg version (use -v), #725
|
- --show-version: shows/logs the borg version, #725
|
||||||
- borg list/prune/delete: also output archive id, #731
|
- borg list/prune/delete: also output archive id, #731
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
@ -70,22 +74,34 @@ Other changes:
|
||||||
- ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945
|
- ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945
|
||||||
|
|
||||||
|
|
||||||
Version 1.0.3 (not released yet)
|
Version 1.0.3
|
||||||
--------------------------------
|
-------------
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
- prune: ignore checkpoints, #997
|
- prune: avoid that checkpoints are kept and completed archives are deleted in
|
||||||
- prune: fix bad validator, #942
|
a prune run), #997
|
||||||
- fix capabilities extraction on Linux (set xattrs last, after chown())
|
- prune: fix commandline argument validation - some valid command lines were
|
||||||
|
considered invalid (annoying, but harmless), #942
|
||||||
|
- fix capabilities extraction on Linux (set xattrs last, after chown()), #1069
|
||||||
|
- repository: fix commit tags being seen in data
|
||||||
|
- when probing key files, do binary reads. avoids crash when non-borg binary
|
||||||
|
files are located in borg's key files directory.
|
||||||
|
- handle SIGTERM and make a clean exit - avoids orphan lock files.
|
||||||
|
- repository cache: don't cache large objects (avoid using lots of temp. disk
|
||||||
|
space), #1063
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
|
|
||||||
- update readthedocs URLs, #991
|
|
||||||
- add missing docs for "borg break-lock", #992
|
|
||||||
- borg create help: add some words to about the archive name
|
|
||||||
- borg create help: document format tags, #894
|
|
||||||
- Vagrantfile: OS X: update osxfuse / install lzma package, #933
|
- Vagrantfile: OS X: update osxfuse / install lzma package, #933
|
||||||
|
- setup.py: add check for platform_darwin.c
|
||||||
|
- setup.py: on freebsd, use a llfuse release that builds ok
|
||||||
|
- docs / help:
|
||||||
|
|
||||||
|
- update readthedocs URLs, #991
|
||||||
|
- add missing docs for "borg break-lock", #992
|
||||||
|
- borg create help: add some words to about the archive name
|
||||||
|
- borg create help: document format tags, #894
|
||||||
|
|
||||||
|
|
||||||
Version 1.0.2
|
Version 1.0.2
|
||||||
|
|
|
@ -27,8 +27,10 @@ errors, critical for critical errors/states).
|
||||||
When directly talking to the user (e.g. Y/N questions), do not use logging,
|
When directly talking to the user (e.g. Y/N questions), do not use logging,
|
||||||
but directly output to stderr (not: stdout, it could be connected to a pipe).
|
but directly output to stderr (not: stdout, it could be connected to a pipe).
|
||||||
|
|
||||||
To control the amount and kinds of messages output to stderr or emitted at
|
To control the amount and kinds of messages output emitted at info level, use
|
||||||
info level, use flags like ``--stats`` or ``--list``.
|
flags like ``--stats`` or ``--list``, then create a topic logger for messages
|
||||||
|
controlled by that flag. See ``_setup_implied_logging()`` in
|
||||||
|
``borg/archiver.py`` for the entry point to topic logging.
|
||||||
|
|
||||||
Building a development environment
|
Building a development environment
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
|
@ -46,7 +46,7 @@ A step by step example
|
||||||
|
|
||||||
3. The next day create a new archive called *Tuesday*::
|
3. The next day create a new archive called *Tuesday*::
|
||||||
|
|
||||||
$ borg create -v --stats /path/to/repo::Tuesday ~/src ~/Documents
|
$ borg create --stats /path/to/repo::Tuesday ~/src ~/Documents
|
||||||
|
|
||||||
This backup will be a lot quicker and a lot smaller since only new never
|
This backup will be a lot quicker and a lot smaller since only new never
|
||||||
before seen data is stored. The ``--stats`` option causes |project_name| to
|
before seen data is stored. The ``--stats`` option causes |project_name| to
|
||||||
|
@ -93,9 +93,10 @@ A step by step example
|
||||||
|
|
||||||
.. Note::
|
.. Note::
|
||||||
Borg is quiet by default (it works on WARNING log level).
|
Borg is quiet by default (it works on WARNING log level).
|
||||||
Add the ``-v`` (or ``--verbose`` or ``--info``) option to adjust the log
|
You can use options like ``--progress`` or ``--list`` to get specific
|
||||||
level to INFO and also use options like ``--progress`` or ``--list`` to
|
reports during command execution. You can also add the ``-v`` (or
|
||||||
get progress reporting during command execution.
|
``--verbose`` or ``--info``) option to adjust the log level to INFO to
|
||||||
|
get other informational messages.
|
||||||
|
|
||||||
Automating backups
|
Automating backups
|
||||||
------------------
|
------------------
|
||||||
|
@ -105,23 +106,27 @@ server. The script also uses the :ref:`borg_prune` subcommand to maintain a
|
||||||
certain number of old archives::
|
certain number of old archives::
|
||||||
|
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
REPOSITORY=username@remoteserver.com:backup
|
|
||||||
|
|
||||||
# Backup all of /home and /var/www except a few
|
# setting this, so the repo does not need to be given on the commandline:
|
||||||
# excluded directories
|
export BORG_REPO=username@remoteserver.com:backup
|
||||||
borg create -v --stats \
|
|
||||||
$REPOSITORY::`hostname`-`date +%Y-%m-%d` \
|
# setting this, so you won't be asked for your passphrase - make sure the
|
||||||
/home \
|
# script has appropriate owner/group and mode, e.g. root.root 600:
|
||||||
/var/www \
|
export BORG_PASSPHRASE=mysecret
|
||||||
--exclude '/home/*/.cache' \
|
|
||||||
--exclude /home/Ben/Music/Justin\ Bieber \
|
# Backup most important stuff:
|
||||||
|
borg create --stats -C lz4 ::`hostname`-`date +%Y-%m-%d` \
|
||||||
|
/etc \
|
||||||
|
/home \
|
||||||
|
/var \
|
||||||
|
--exclude '/home/*/.cache' \
|
||||||
--exclude '*.pyc'
|
--exclude '*.pyc'
|
||||||
|
|
||||||
# Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly
|
# Use the `prune` subcommand to maintain 7 daily, 4 weekly and 6 monthly
|
||||||
# archives of THIS machine. --prefix `hostname`- is very important to
|
# archives of THIS machine. Using --prefix is very important to
|
||||||
# limit prune's operation to this machine's archives and not apply to
|
# limit prune's operation to this machine's archives and not apply to
|
||||||
# other machine's archives also.
|
# other machine's archives also.
|
||||||
borg prune -v $REPOSITORY --prefix `hostname`- \
|
borg prune -v --prefix `hostname`- \
|
||||||
--keep-daily=7 --keep-weekly=4 --keep-monthly=6
|
--keep-daily=7 --keep-weekly=4 --keep-monthly=6
|
||||||
|
|
||||||
.. backup_compression:
|
.. backup_compression:
|
||||||
|
|
|
@ -16,7 +16,8 @@ Type of log output
|
||||||
|
|
||||||
The log level of the builtin logging configuration defaults to WARNING.
|
The log level of the builtin logging configuration defaults to WARNING.
|
||||||
This is because we want |project_name| to be mostly silent and only output
|
This is because we want |project_name| to be mostly silent and only output
|
||||||
warnings, errors and critical messages.
|
warnings, errors and critical messages, unless output has been requested
|
||||||
|
by supplying an option that implies output (eg, --list or --progress).
|
||||||
|
|
||||||
Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
|
Log levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
|
||||||
|
|
||||||
|
@ -41,10 +42,6 @@ give different output on different log levels - it's just a possibility.
|
||||||
.. warning:: Options --critical and --error are provided for completeness,
|
.. warning:: Options --critical and --error are provided for completeness,
|
||||||
their usage is not recommended as you might miss important information.
|
their usage is not recommended as you might miss important information.
|
||||||
|
|
||||||
.. warning:: While some options (like ``--stats`` or ``--list``) will emit more
|
|
||||||
informational messages, you have to use INFO (or lower) log level to make
|
|
||||||
them show up in log output. Use ``-v`` or a logging configuration.
|
|
||||||
|
|
||||||
Return codes
|
Return codes
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -170,6 +167,22 @@ Network:
|
||||||
|
|
||||||
In case you are interested in more details, please read the internals documentation.
|
In case you are interested in more details, please read the internals documentation.
|
||||||
|
|
||||||
|
File systems
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We strongly recommend against using Borg (or any other database-like
|
||||||
|
software) on non-journaling file systems like FAT, since it is not
|
||||||
|
possible to assume any consistency in case of power failures (or a
|
||||||
|
sudden disconnect of an external drive or similar failures).
|
||||||
|
|
||||||
|
While Borg uses a data store that is resilient against these failures
|
||||||
|
when used on journaling file systems, it is not possible to guarantee
|
||||||
|
this with some hardware -- independent of the software used. We don't
|
||||||
|
know a list of affected hardware.
|
||||||
|
|
||||||
|
If you are suspicious whether your Borg repository is still consistent
|
||||||
|
and readable after one of the failures mentioned above occured, run
|
||||||
|
``borg check --verify-data`` to make sure it is consistent.
|
||||||
|
|
||||||
Units
|
Units
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -220,46 +233,6 @@ Examples
|
||||||
# Remote repository (store the key your home dir)
|
# Remote repository (store the key your home dir)
|
||||||
$ borg init --encryption=keyfile user@hostname:backup
|
$ borg init --encryption=keyfile user@hostname:backup
|
||||||
|
|
||||||
Important notes about encryption:
|
|
||||||
|
|
||||||
It is not recommended to disable encryption. Repository encryption protects you
|
|
||||||
e.g. against the case that an attacker has access to your backup repository.
|
|
||||||
|
|
||||||
But be careful with the key / the passphrase:
|
|
||||||
|
|
||||||
If you want "passphrase-only" security, use the ``repokey`` mode. The key will
|
|
||||||
be stored inside the repository (in its "config" file). In above mentioned
|
|
||||||
attack scenario, the attacker will have the key (but not the passphrase).
|
|
||||||
|
|
||||||
If you want "passphrase and having-the-key" security, use the ``keyfile`` mode.
|
|
||||||
The key will be stored in your home directory (in ``.config/borg/keys``). In
|
|
||||||
the attack scenario, the attacker who has just access to your repo won't have
|
|
||||||
the key (and also not the passphrase).
|
|
||||||
|
|
||||||
Make a backup copy of the key file (``keyfile`` mode) or repo config file
|
|
||||||
(``repokey`` mode) and keep it at a safe place, so you still have the key in
|
|
||||||
case it gets corrupted or lost. Also keep the passphrase at a safe place.
|
|
||||||
The backup that is encrypted with that key won't help you with that, of course.
|
|
||||||
|
|
||||||
Make sure you use a good passphrase. Not too short, not too simple. The real
|
|
||||||
encryption / decryption key is encrypted with / locked by your passphrase.
|
|
||||||
If an attacker gets your key, he can't unlock and use it without knowing the
|
|
||||||
passphrase.
|
|
||||||
|
|
||||||
Be careful with special or non-ascii characters in your passphrase:
|
|
||||||
|
|
||||||
- |project_name| processes the passphrase as unicode (and encodes it as utf-8),
|
|
||||||
so it does not have problems dealing with even the strangest characters.
|
|
||||||
- BUT: that does not necessarily apply to your OS / VM / keyboard configuration.
|
|
||||||
|
|
||||||
So better use a long passphrase made from simple ascii chars than one that
|
|
||||||
includes non-ascii stuff or characters that are hard/impossible to enter on
|
|
||||||
a different keyboard layout.
|
|
||||||
|
|
||||||
You can change your passphrase for existing repos at any time, it won't affect
|
|
||||||
the encryption/decryption key or other secrets.
|
|
||||||
|
|
||||||
|
|
||||||
.. include:: usage/create.rst.inc
|
.. include:: usage/create.rst.inc
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
|
@ -269,8 +242,8 @@ Examples
|
||||||
# Backup ~/Documents into an archive named "my-documents"
|
# Backup ~/Documents into an archive named "my-documents"
|
||||||
$ borg create /path/to/repo::my-documents ~/Documents
|
$ borg create /path/to/repo::my-documents ~/Documents
|
||||||
|
|
||||||
# same, but verbosely list all files as we process them
|
# same, but list all files as we process them
|
||||||
$ borg create -v --list /path/to/repo::my-documents ~/Documents
|
$ borg create --list /path/to/repo::my-documents ~/Documents
|
||||||
|
|
||||||
# Backup ~/Documents and ~/src but exclude pyc files
|
# Backup ~/Documents and ~/src but exclude pyc files
|
||||||
$ borg create /path/to/repo::my-files \
|
$ borg create /path/to/repo::my-files \
|
||||||
|
@ -325,7 +298,10 @@ Examples
|
||||||
$ borg extract /path/to/repo::my-files
|
$ borg extract /path/to/repo::my-files
|
||||||
|
|
||||||
# Extract entire archive and list files while processing
|
# Extract entire archive and list files while processing
|
||||||
$ borg extract -v --list /path/to/repo::my-files
|
$ borg extract --list /path/to/repo::my-files
|
||||||
|
|
||||||
|
# Verify whether an archive could be successfully extracted, but do not write files to disk
|
||||||
|
$ borg extract --dry-run /path/to/repo::my-files
|
||||||
|
|
||||||
# Extract the "src" directory
|
# Extract the "src" directory
|
||||||
$ borg extract /path/to/repo::my-files home/USERNAME/src
|
$ borg extract /path/to/repo::my-files home/USERNAME/src
|
||||||
|
@ -501,7 +477,7 @@ Examples
|
||||||
Username: root
|
Username: root
|
||||||
Time (start): Mon, 2016-02-15 19:36:29
|
Time (start): Mon, 2016-02-15 19:36:29
|
||||||
Time (end): Mon, 2016-02-15 19:39:26
|
Time (end): Mon, 2016-02-15 19:39:26
|
||||||
Command line: /usr/local/bin/borg create -v --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system
|
Command line: /usr/local/bin/borg create --list -C zlib,6 /path/to/repo::root-2016-02-15 / --one-file-system
|
||||||
Number of files: 38100
|
Number of files: 38100
|
||||||
|
|
||||||
Original size Compressed size Deduplicated size
|
Original size Compressed size Deduplicated size
|
||||||
|
@ -676,7 +652,7 @@ Here are misc. notes about topics that are maybe not covered in enough detail in
|
||||||
Item flags
|
Item flags
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
``borg create -v --list`` outputs a verbose list of all files, directories and other
|
``borg create --list`` outputs a list of all files, directories and other
|
||||||
file system items it considered (no matter whether they had content changes
|
file system items it considered (no matter whether they had content changes
|
||||||
or not). For each item, it prefixes a single-letter flag that indicates type
|
or not). For each item, it prefixes a single-letter flag that indicates type
|
||||||
and/or status of the item.
|
and/or status of the item.
|
||||||
|
|
|
@ -74,6 +74,12 @@ This command creates a backup archive containing all files found while recursive
|
||||||
traversing all paths specified. The archive will consume almost no disk space for
|
traversing all paths specified. The archive will consume almost no disk space for
|
||||||
files or parts of files that have already been stored in other archives.
|
files or parts of files that have already been stored in other archives.
|
||||||
|
|
||||||
|
The archive name needs to be unique. It must not end in '.checkpoint' or
|
||||||
|
'.checkpoint.N' (with N being a number), because these names are used for
|
||||||
|
checkpoints and treated in special ways.
|
||||||
|
|
||||||
|
In the archive name, you may use the following format tags:
|
||||||
|
{now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}
|
||||||
|
|
||||||
To speed up pulling backups over sshfs and similar network file systems which do
|
To speed up pulling backups over sshfs and similar network file systems which do
|
||||||
not provide correct inode information the --ignore-inode flag can be used. This
|
not provide correct inode information the --ignore-inode flag can be used. This
|
||||||
|
|
21
setup.py
21
setup.py
|
@ -26,20 +26,26 @@ install_requires = ['msgpack-python>=0.4.6', ]
|
||||||
extras_require = {
|
extras_require = {
|
||||||
# llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
|
# llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
|
||||||
# llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
# llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
||||||
|
# llfuse 0.41.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
||||||
# llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
# llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
||||||
# llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
# llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0
|
||||||
# llfuse 2.0 will break API
|
# llfuse 2.0 will break API
|
||||||
'fuse': ['llfuse<2.0', ],
|
'fuse': ['llfuse<2.0', ],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sys.platform.startswith('freebsd'):
|
||||||
|
# while llfuse 1.0 is the latest llfuse release right now,
|
||||||
|
# llfuse 0.41.1 is the latest release that actually builds on freebsd:
|
||||||
|
extras_require['fuse'] = ['llfuse==0.41.1', ]
|
||||||
|
|
||||||
from setuptools import setup, find_packages, Extension
|
from setuptools import setup, find_packages, Extension
|
||||||
from setuptools.command.sdist import sdist
|
from setuptools.command.sdist import sdist
|
||||||
|
|
||||||
|
|
||||||
compress_source = 'src/borg/compress.pyx'
|
compress_source = 'src/borg/compress.pyx'
|
||||||
crypto_source = 'src/borg/crypto.pyx'
|
crypto_source = 'src/borg/crypto.pyx'
|
||||||
chunker_source = 'src/borg/chunker.pyx'
|
chunker_source = 'src/borg/chunker.pyx'
|
||||||
hashindex_source = 'src/borg/hashindex.pyx'
|
hashindex_source = 'src/borg/hashindex.pyx'
|
||||||
|
platform_posix_source = 'src/borg/platform_posix.pyx'
|
||||||
platform_linux_source = 'src/borg/platform_linux.pyx'
|
platform_linux_source = 'src/borg/platform_linux.pyx'
|
||||||
platform_darwin_source = 'src/borg/platform_darwin.pyx'
|
platform_darwin_source = 'src/borg/platform_darwin.pyx'
|
||||||
platform_freebsd_source = 'src/borg/platform_freebsd.pyx'
|
platform_freebsd_source = 'src/borg/platform_freebsd.pyx'
|
||||||
|
@ -60,6 +66,7 @@ try:
|
||||||
'src/borg/crypto.c',
|
'src/borg/crypto.c',
|
||||||
'src/borg/chunker.c', 'src/borg/_chunker.c',
|
'src/borg/chunker.c', 'src/borg/_chunker.c',
|
||||||
'src/borg/hashindex.c', 'src/borg/_hashindex.c',
|
'src/borg/hashindex.c', 'src/borg/_hashindex.c',
|
||||||
|
'src/borg/platform_posix.c',
|
||||||
'src/borg/platform_linux.c',
|
'src/borg/platform_linux.c',
|
||||||
'src/borg/platform_freebsd.c',
|
'src/borg/platform_freebsd.c',
|
||||||
'src/borg/platform_darwin.c',
|
'src/borg/platform_darwin.c',
|
||||||
|
@ -75,13 +82,14 @@ except ImportError:
|
||||||
crypto_source = crypto_source.replace('.pyx', '.c')
|
crypto_source = crypto_source.replace('.pyx', '.c')
|
||||||
chunker_source = chunker_source.replace('.pyx', '.c')
|
chunker_source = chunker_source.replace('.pyx', '.c')
|
||||||
hashindex_source = hashindex_source.replace('.pyx', '.c')
|
hashindex_source = hashindex_source.replace('.pyx', '.c')
|
||||||
|
platform_posix_source = platform_posix_source.replace('.pyx', '.c')
|
||||||
platform_linux_source = platform_linux_source.replace('.pyx', '.c')
|
platform_linux_source = platform_linux_source.replace('.pyx', '.c')
|
||||||
platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c')
|
platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c')
|
||||||
platform_darwin_source = platform_darwin_source.replace('.pyx', '.c')
|
platform_darwin_source = platform_darwin_source.replace('.pyx', '.c')
|
||||||
from distutils.command.build_ext import build_ext
|
from distutils.command.build_ext import build_ext
|
||||||
if not on_rtd and not all(os.path.exists(path) for path in [
|
if not on_rtd and not all(os.path.exists(path) for path in [
|
||||||
compress_source, crypto_source, chunker_source, hashindex_source,
|
compress_source, crypto_source, chunker_source, hashindex_source,
|
||||||
platform_linux_source, platform_freebsd_source]):
|
platform_posix_source, platform_linux_source, platform_freebsd_source, platform_darwin_source]):
|
||||||
raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.')
|
raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.')
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,7 +114,8 @@ def detect_lz4(prefixes):
|
||||||
include_dirs = []
|
include_dirs = []
|
||||||
library_dirs = []
|
library_dirs = []
|
||||||
|
|
||||||
possible_openssl_prefixes = ['/usr', '/usr/local', '/usr/local/opt/openssl', '/usr/local/ssl', '/usr/local/openssl', '/usr/local/borg', '/opt/local']
|
possible_openssl_prefixes = ['/usr', '/usr/local', '/usr/local/opt/openssl', '/usr/local/ssl', '/usr/local/openssl',
|
||||||
|
'/usr/local/borg', '/opt/local', '/opt/pkg', ]
|
||||||
if os.environ.get('BORG_OPENSSL_PREFIX'):
|
if os.environ.get('BORG_OPENSSL_PREFIX'):
|
||||||
possible_openssl_prefixes.insert(0, os.environ.get('BORG_OPENSSL_PREFIX'))
|
possible_openssl_prefixes.insert(0, os.environ.get('BORG_OPENSSL_PREFIX'))
|
||||||
ssl_prefix = detect_openssl(possible_openssl_prefixes)
|
ssl_prefix = detect_openssl(possible_openssl_prefixes)
|
||||||
|
@ -116,7 +125,8 @@ include_dirs.append(os.path.join(ssl_prefix, 'include'))
|
||||||
library_dirs.append(os.path.join(ssl_prefix, 'lib'))
|
library_dirs.append(os.path.join(ssl_prefix, 'lib'))
|
||||||
|
|
||||||
|
|
||||||
possible_lz4_prefixes = ['/usr', '/usr/local', '/usr/local/opt/lz4', '/usr/local/lz4', '/usr/local/borg', '/opt/local']
|
possible_lz4_prefixes = ['/usr', '/usr/local', '/usr/local/opt/lz4', '/usr/local/lz4',
|
||||||
|
'/usr/local/borg', '/opt/local', '/opt/pkg', ]
|
||||||
if os.environ.get('BORG_LZ4_PREFIX'):
|
if os.environ.get('BORG_LZ4_PREFIX'):
|
||||||
possible_lz4_prefixes.insert(0, os.environ.get('BORG_LZ4_PREFIX'))
|
possible_lz4_prefixes.insert(0, os.environ.get('BORG_LZ4_PREFIX'))
|
||||||
lz4_prefix = detect_lz4(possible_lz4_prefixes)
|
lz4_prefix = detect_lz4(possible_lz4_prefixes)
|
||||||
|
@ -284,6 +294,9 @@ if not on_rtd:
|
||||||
Extension('borg.chunker', [chunker_source]),
|
Extension('borg.chunker', [chunker_source]),
|
||||||
Extension('borg.hashindex', [hashindex_source])
|
Extension('borg.hashindex', [hashindex_source])
|
||||||
]
|
]
|
||||||
|
if sys.platform.startswith(('linux', 'freebsd', 'darwin')):
|
||||||
|
ext_modules.append(Extension('borg.platform_posix', [platform_posix_source]))
|
||||||
|
|
||||||
if sys.platform == 'linux':
|
if sys.platform == 'linux':
|
||||||
ext_modules.append(Extension('borg.platform_linux', [platform_linux_source], libraries=['acl']))
|
ext_modules.append(Extension('borg.platform_linux', [platform_linux_source], libraries=['acl']))
|
||||||
elif sys.platform.startswith('freebsd'):
|
elif sys.platform.startswith('freebsd'):
|
||||||
|
|
|
@ -184,9 +184,9 @@ chunker_fill(Chunker *c)
|
||||||
length = c->bytes_read - offset;
|
length = c->bytes_read - offset;
|
||||||
#if ( ( _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L ) && defined(POSIX_FADV_DONTNEED) )
|
#if ( ( _XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L ) && defined(POSIX_FADV_DONTNEED) )
|
||||||
|
|
||||||
// Only do it once per run.
|
// Only do it once per run.
|
||||||
if (pagemask == 0)
|
if (pagemask == 0)
|
||||||
pagemask = getpagesize() - 1;
|
pagemask = getpagesize() - 1;
|
||||||
|
|
||||||
// We tell the OS that we do not need the data that we just have read any
|
// We tell the OS that we do not need the data that we just have read any
|
||||||
// more (that it maybe has in the cache). This avoids that we spoil the
|
// more (that it maybe has in the cache). This avoids that we spoil the
|
||||||
|
@ -207,7 +207,7 @@ chunker_fill(Chunker *c)
|
||||||
// fadvise. This will cancel the final page and is not part
|
// fadvise. This will cancel the final page and is not part
|
||||||
// of the above workaround.
|
// of the above workaround.
|
||||||
overshoot = 0;
|
overshoot = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
posix_fadvise(c->fh, offset & ~pagemask, length - overshoot, POSIX_FADV_DONTNEED);
|
posix_fadvise(c->fh, offset & ~pagemask, length - overshoot, POSIX_FADV_DONTNEED);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .key import key_factory
|
||||||
from .remote import cache_if_remote
|
from .remote import cache_if_remote
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from shutil import get_terminal_size
|
||||||
import socket
|
import socket
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
@ -19,24 +20,76 @@ from .compress import COMPR_BUFFER
|
||||||
from .constants import * # NOQA
|
from .constants import * # NOQA
|
||||||
from .helpers import Chunk, Error, uid2user, user2uid, gid2group, group2gid, \
|
from .helpers import Chunk, Error, uid2user, user2uid, gid2group, group2gid, \
|
||||||
parse_timestamp, to_localtime, format_time, format_timedelta, safe_encode, safe_decode, \
|
parse_timestamp, to_localtime, format_time, format_timedelta, safe_encode, safe_decode, \
|
||||||
Manifest, Statistics, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, bin_to_hex, \
|
Manifest, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, bin_to_hex, \
|
||||||
ProgressIndicatorPercent, ChunkIteratorFileWrapper, remove_surrogates, log_multi, \
|
ProgressIndicatorPercent, ChunkIteratorFileWrapper, remove_surrogates, log_multi, \
|
||||||
PathPrefixPattern, FnmatchPattern, open_item, file_status, format_file_size, consume, \
|
PathPrefixPattern, FnmatchPattern, open_item, file_status, format_file_size, consume, \
|
||||||
CompressionDecider1, CompressionDecider2, CompressionSpec
|
CompressionDecider1, CompressionDecider2, CompressionSpec, \
|
||||||
|
IntegrityError
|
||||||
from .repository import Repository
|
from .repository import Repository
|
||||||
from .platform import acl_get, acl_set
|
from .platform import acl_get, acl_set, set_flags, get_flags, swidth
|
||||||
from .chunker import Chunker
|
from .chunker import Chunker
|
||||||
from .hashindex import ChunkIndex, ChunkIndexEntry
|
from .hashindex import ChunkIndex, ChunkIndexEntry
|
||||||
from .cache import ChunkListEntry
|
from .cache import ChunkListEntry
|
||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
has_lchmod = hasattr(os, 'lchmod')
|
has_lchmod = hasattr(os, 'lchmod')
|
||||||
has_lchflags = hasattr(os, 'lchflags')
|
|
||||||
|
|
||||||
flags_normal = os.O_RDONLY | getattr(os, 'O_BINARY', 0)
|
flags_normal = os.O_RDONLY | getattr(os, 'O_BINARY', 0)
|
||||||
flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
|
flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
|
||||||
|
|
||||||
|
|
||||||
|
class Statistics:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.osize = self.csize = self.usize = self.nfiles = 0
|
||||||
|
self.last_progress = 0 # timestamp when last progress was shown
|
||||||
|
|
||||||
|
def update(self, size, csize, unique):
|
||||||
|
self.osize += size
|
||||||
|
self.csize += csize
|
||||||
|
if unique:
|
||||||
|
self.usize += csize
|
||||||
|
|
||||||
|
summary = """\
|
||||||
|
Original size Compressed size Deduplicated size
|
||||||
|
{label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.summary.format(stats=self, label='This archive:')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>".format(
|
||||||
|
cls=type(self).__name__, hash=id(self), self=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def osize_fmt(self):
|
||||||
|
return format_file_size(self.osize)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def usize_fmt(self):
|
||||||
|
return format_file_size(self.usize)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def csize_fmt(self):
|
||||||
|
return format_file_size(self.csize)
|
||||||
|
|
||||||
|
def show_progress(self, item=None, final=False, stream=None, dt=None):
|
||||||
|
now = time.time()
|
||||||
|
if dt is None or now - self.last_progress > dt:
|
||||||
|
self.last_progress = now
|
||||||
|
columns, lines = get_terminal_size()
|
||||||
|
if not final:
|
||||||
|
msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self)
|
||||||
|
path = remove_surrogates(item[b'path']) if item else ''
|
||||||
|
space = columns - swidth(msg)
|
||||||
|
if space < swidth('...') + swidth(path):
|
||||||
|
path = '%s...%s' % (path[:(space // 2) - swidth('...')], path[-space // 2:])
|
||||||
|
msg += "{0:<{space}}".format(path, space=space)
|
||||||
|
else:
|
||||||
|
msg = ' ' * columns
|
||||||
|
print(msg, file=stream or sys.stderr, end="\r", flush=True)
|
||||||
|
|
||||||
|
|
||||||
class DownloadPipeline:
|
class DownloadPipeline:
|
||||||
|
|
||||||
def __init__(self, repository, key):
|
def __init__(self, repository, key):
|
||||||
|
@ -434,10 +487,9 @@ Number of files: {0.stats.nfiles}'''.format(
|
||||||
else:
|
else:
|
||||||
os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)
|
os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)
|
||||||
acl_set(path, item, self.numeric_owner)
|
acl_set(path, item, self.numeric_owner)
|
||||||
# Only available on OS X and FreeBSD
|
if b'bsdflags' in item:
|
||||||
if has_lchflags and b'bsdflags' in item:
|
|
||||||
try:
|
try:
|
||||||
os.lchflags(path, item[b'bsdflags'])
|
set_flags(path, item[b'bsdflags'], fd=fd)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
# chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
|
# chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
|
||||||
|
@ -505,8 +557,9 @@ Number of files: {0.stats.nfiles}'''.format(
|
||||||
xattrs = xattr.get_all(path, follow_symlinks=False)
|
xattrs = xattr.get_all(path, follow_symlinks=False)
|
||||||
if xattrs:
|
if xattrs:
|
||||||
item[b'xattrs'] = StableDict(xattrs)
|
item[b'xattrs'] = StableDict(xattrs)
|
||||||
if has_lchflags and st.st_flags:
|
bsdflags = get_flags(path, st)
|
||||||
item[b'bsdflags'] = st.st_flags
|
if bsdflags:
|
||||||
|
item[b'bsdflags'] = bsdflags
|
||||||
acl_get(path, item, st, self.numeric_owner)
|
acl_get(path, item, st, self.numeric_owner)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
@ -698,7 +751,17 @@ class ArchiveChecker:
|
||||||
self.error_found = False
|
self.error_found = False
|
||||||
self.possibly_superseded = set()
|
self.possibly_superseded = set()
|
||||||
|
|
||||||
def check(self, repository, repair=False, archive=None, last=None, prefix=None, save_space=False):
|
def check(self, repository, repair=False, archive=None, last=None, prefix=None, verify_data=False,
|
||||||
|
save_space=False):
|
||||||
|
"""Perform a set of checks on 'repository'
|
||||||
|
|
||||||
|
:param repair: enable repair mode, write updated or corrected data into repository
|
||||||
|
:param archive: only check this archive
|
||||||
|
:param last: only check this number of recent archives
|
||||||
|
:param prefix: only check archives with this prefix
|
||||||
|
:param verify_data: integrity verification of data referenced by archives
|
||||||
|
:param save_space: Repository.commit(save_space)
|
||||||
|
"""
|
||||||
logger.info('Starting archive consistency check...')
|
logger.info('Starting archive consistency check...')
|
||||||
self.check_all = archive is None and last is None and prefix is None
|
self.check_all = archive is None and last is None and prefix is None
|
||||||
self.repair = repair
|
self.repair = repair
|
||||||
|
@ -712,6 +775,8 @@ class ArchiveChecker:
|
||||||
else:
|
else:
|
||||||
self.manifest, _ = Manifest.load(repository, key=self.key)
|
self.manifest, _ = Manifest.load(repository, key=self.key)
|
||||||
self.rebuild_refcounts(archive=archive, last=last, prefix=prefix)
|
self.rebuild_refcounts(archive=archive, last=last, prefix=prefix)
|
||||||
|
if verify_data:
|
||||||
|
self.verify_data()
|
||||||
self.orphan_chunks_check()
|
self.orphan_chunks_check()
|
||||||
self.finish(save_space=save_space)
|
self.finish(save_space=save_space)
|
||||||
if self.error_found:
|
if self.error_found:
|
||||||
|
@ -741,6 +806,26 @@ class ArchiveChecker:
|
||||||
cdata = repository.get(next(self.chunks.iteritems())[0])
|
cdata = repository.get(next(self.chunks.iteritems())[0])
|
||||||
return key_factory(repository, cdata)
|
return key_factory(repository, cdata)
|
||||||
|
|
||||||
|
def verify_data(self):
|
||||||
|
logger.info('Starting cryptographic data integrity verification...')
|
||||||
|
pi = ProgressIndicatorPercent(total=len(self.chunks), msg="Verifying data %6.2f%%", step=0.01, same_line=True)
|
||||||
|
count = errors = 0
|
||||||
|
for chunk_id, (refcount, *_) in self.chunks.iteritems():
|
||||||
|
pi.show()
|
||||||
|
if not refcount:
|
||||||
|
continue
|
||||||
|
encrypted_data = self.repository.get(chunk_id)
|
||||||
|
try:
|
||||||
|
_, data = self.key.decrypt(chunk_id, encrypted_data)
|
||||||
|
except IntegrityError as integrity_error:
|
||||||
|
self.error_found = True
|
||||||
|
errors += 1
|
||||||
|
logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error)
|
||||||
|
count += 1
|
||||||
|
pi.finish()
|
||||||
|
log = logger.error if errors else logger.info
|
||||||
|
log('Finished cryptographic data integrity verification, verified %d chunks with %d integrity errors.', count, errors)
|
||||||
|
|
||||||
def rebuild_manifest(self):
|
def rebuild_manifest(self):
|
||||||
"""Rebuild the manifest object if it is missing
|
"""Rebuild the manifest object if it is missing
|
||||||
|
|
||||||
|
@ -874,6 +959,8 @@ class ArchiveChecker:
|
||||||
else:
|
else:
|
||||||
# we only want one specific archive
|
# we only want one specific archive
|
||||||
archive_items = [item for item in self.manifest.archives.items() if item[0] == archive]
|
archive_items = [item for item in self.manifest.archives.items() if item[0] == archive]
|
||||||
|
if not archive_items:
|
||||||
|
logger.error("Archive '%s' not found.", archive)
|
||||||
num_archives = 1
|
num_archives = 1
|
||||||
end = 1
|
end = 1
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import inspect
|
import inspect
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
@ -22,7 +23,7 @@ from . import __version__
|
||||||
from .helpers import Error, location_validator, archivename_validator, format_time, format_file_size, \
|
from .helpers import Error, location_validator, archivename_validator, format_time, format_file_size, \
|
||||||
parse_pattern, PathPrefixPattern, to_localtime, timestamp, \
|
parse_pattern, PathPrefixPattern, to_localtime, timestamp, \
|
||||||
get_cache_dir, prune_within, prune_split, bin_to_hex, safe_encode, \
|
get_cache_dir, prune_within, prune_split, bin_to_hex, safe_encode, \
|
||||||
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
|
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, \
|
||||||
dir_is_tagged, ChunkerParams, CompressionSpec, is_slow_msgpack, yes, sysinfo, \
|
dir_is_tagged, ChunkerParams, CompressionSpec, is_slow_msgpack, yes, sysinfo, \
|
||||||
log_multi, PatternMatcher, ItemFormatter
|
log_multi, PatternMatcher, ItemFormatter
|
||||||
from .logger import create_logger, setup_logging
|
from .logger import create_logger, setup_logging
|
||||||
|
@ -34,12 +35,11 @@ from .repository import Repository
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .constants import * # NOQA
|
from .constants import * # NOQA
|
||||||
from .key import key_creator, RepoKey, PassphraseKey
|
from .key import key_creator, RepoKey, PassphraseKey
|
||||||
from .archive import Archive, ArchiveChecker, ArchiveRecreater
|
from .archive import Archive, ArchiveChecker, ArchiveRecreater, Statistics
|
||||||
from .remote import RepositoryServer, RemoteRepository, cache_if_remote
|
from .remote import RepositoryServer, RemoteRepository, cache_if_remote
|
||||||
from .selftest import selftest
|
from .selftest import selftest
|
||||||
from .hashindex import ChunkIndexEntry
|
from .hashindex import ChunkIndexEntry
|
||||||
|
from .platform import get_flags
|
||||||
has_lchflags = hasattr(os, 'lchflags')
|
|
||||||
|
|
||||||
|
|
||||||
def argument(args, str_or_bool):
|
def argument(args, str_or_bool):
|
||||||
|
@ -112,7 +112,7 @@ class Archiver:
|
||||||
|
|
||||||
def print_file_status(self, status, path):
|
def print_file_status(self, status, path):
|
||||||
if self.output_list and (self.output_filter is None or status in self.output_filter):
|
if self.output_list and (self.output_filter is None or status in self.output_filter):
|
||||||
logger.info("%1s %s", status, remove_surrogates(path))
|
logging.getLogger('borg.output.list').info("%1s %s", status, remove_surrogates(path))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compare_chunk_contents(chunks1, chunks2):
|
def compare_chunk_contents(chunks1, chunks2):
|
||||||
|
@ -185,12 +185,16 @@ class Archiver:
|
||||||
if not yes(msg, false_msg="Aborting.", truish=('YES', ),
|
if not yes(msg, false_msg="Aborting.", truish=('YES', ),
|
||||||
env_var_override='BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
env_var_override='BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
||||||
return EXIT_ERROR
|
return EXIT_ERROR
|
||||||
|
if args.repo_only and args.verify_data:
|
||||||
|
self.print_error("--repository-only and --verify-data contradict each other. Please select one.")
|
||||||
|
return EXIT_ERROR
|
||||||
if not args.archives_only:
|
if not args.archives_only:
|
||||||
if not repository.check(repair=args.repair, save_space=args.save_space):
|
if not repository.check(repair=args.repair, save_space=args.save_space):
|
||||||
return EXIT_WARNING
|
return EXIT_WARNING
|
||||||
if not args.repo_only and not ArchiveChecker().check(
|
if not args.repo_only and not ArchiveChecker().check(
|
||||||
repository, repair=args.repair, archive=args.location.archive,
|
repository, repair=args.repair, archive=args.location.archive,
|
||||||
last=args.last, prefix=args.prefix, save_space=args.save_space):
|
last=args.last, prefix=args.prefix, verify_data=args.verify_data,
|
||||||
|
save_space=args.save_space):
|
||||||
return EXIT_WARNING
|
return EXIT_WARNING
|
||||||
return EXIT_SUCCESS
|
return EXIT_SUCCESS
|
||||||
|
|
||||||
|
@ -274,7 +278,7 @@ class Archiver:
|
||||||
DASHES,
|
DASHES,
|
||||||
str(archive.stats),
|
str(archive.stats),
|
||||||
str(cache),
|
str(cache),
|
||||||
DASHES)
|
DASHES, logger=logging.getLogger('borg.output.stats'))
|
||||||
|
|
||||||
self.output_filter = args.output_filter
|
self.output_filter = args.output_filter
|
||||||
self.output_list = args.output_list
|
self.output_list = args.output_list
|
||||||
|
@ -312,7 +316,7 @@ class Archiver:
|
||||||
return
|
return
|
||||||
status = None
|
status = None
|
||||||
# Ignore if nodump flag is set
|
# Ignore if nodump flag is set
|
||||||
if has_lchflags and (st.st_flags & stat.UF_NODUMP):
|
if get_flags(path, st) & stat.UF_NODUMP:
|
||||||
return
|
return
|
||||||
if stat.S_ISREG(st.st_mode) or read_special and not stat.S_ISDIR(st.st_mode):
|
if stat.S_ISREG(st.st_mode) or read_special and not stat.S_ISDIR(st.st_mode):
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
@ -413,7 +417,7 @@ class Archiver:
|
||||||
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
||||||
archive.extract_item(dirs.pop(-1), stdout=stdout)
|
archive.extract_item(dirs.pop(-1), stdout=stdout)
|
||||||
if output_list:
|
if output_list:
|
||||||
logger.info(remove_surrogates(orig_path))
|
logging.getLogger('borg.output.list').info(remove_surrogates(orig_path))
|
||||||
try:
|
try:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
archive.extract_item(item, dry_run=True)
|
archive.extract_item(item, dry_run=True)
|
||||||
|
@ -670,7 +674,7 @@ class Archiver:
|
||||||
log_multi(DASHES,
|
log_multi(DASHES,
|
||||||
stats.summary.format(label='Deleted data:', stats=stats),
|
stats.summary.format(label='Deleted data:', stats=stats),
|
||||||
str(cache),
|
str(cache),
|
||||||
DASHES)
|
DASHES, logger=logging.getLogger('borg.output.stats'))
|
||||||
else:
|
else:
|
||||||
if not args.cache_only:
|
if not args.cache_only:
|
||||||
msg = []
|
msg = []
|
||||||
|
@ -789,9 +793,8 @@ class Archiver:
|
||||||
is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search
|
is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search
|
||||||
checkpoints = [arch for arch in archives_checkpoints if is_checkpoint(arch.name)]
|
checkpoints = [arch for arch in archives_checkpoints if is_checkpoint(arch.name)]
|
||||||
# keep the latest checkpoint, if there is no later non-checkpoint archive
|
# keep the latest checkpoint, if there is no later non-checkpoint archive
|
||||||
latest_checkpoint = checkpoints[0] if checkpoints else None
|
if archives_checkpoints and checkpoints and archives_checkpoints[0] is checkpoints[0]:
|
||||||
if archives_checkpoints[0] is latest_checkpoint:
|
keep_checkpoints = checkpoints[:1]
|
||||||
keep_checkpoints = [latest_checkpoint, ]
|
|
||||||
else:
|
else:
|
||||||
keep_checkpoints = []
|
keep_checkpoints = []
|
||||||
checkpoints = set(checkpoints)
|
checkpoints = set(checkpoints)
|
||||||
|
@ -818,18 +821,19 @@ class Archiver:
|
||||||
to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints))
|
to_delete = (set(archives) | checkpoints) - (set(keep) | set(keep_checkpoints))
|
||||||
stats = Statistics()
|
stats = Statistics()
|
||||||
with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache:
|
with Cache(repository, key, manifest, do_files=args.cache_files, lock_wait=self.lock_wait) as cache:
|
||||||
|
list_logger = logging.getLogger('borg.output.list')
|
||||||
for archive in archives_checkpoints:
|
for archive in archives_checkpoints:
|
||||||
if archive in to_delete:
|
if archive in to_delete:
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
if args.output_list:
|
if args.output_list:
|
||||||
logger.info('Would prune: %s' % format_archive(archive))
|
list_logger.info('Would prune: %s' % format_archive(archive))
|
||||||
else:
|
else:
|
||||||
if args.output_list:
|
if args.output_list:
|
||||||
logger.info('Pruning archive: %s' % format_archive(archive))
|
list_logger.info('Pruning archive: %s' % format_archive(archive))
|
||||||
Archive(repository, key, manifest, archive.name, cache).delete(stats)
|
Archive(repository, key, manifest, archive.name, cache).delete(stats)
|
||||||
else:
|
else:
|
||||||
if args.output_list:
|
if args.output_list:
|
||||||
logger.info('Keeping archive: %s' % format_archive(archive))
|
list_logger.info('Keeping archive: %s' % format_archive(archive))
|
||||||
if to_delete and not args.dry_run:
|
if to_delete and not args.dry_run:
|
||||||
manifest.write()
|
manifest.write()
|
||||||
repository.commit(save_space=args.save_space)
|
repository.commit(save_space=args.save_space)
|
||||||
|
@ -838,7 +842,7 @@ class Archiver:
|
||||||
log_multi(DASHES,
|
log_multi(DASHES,
|
||||||
stats.summary.format(label='Deleted data:', stats=stats),
|
stats.summary.format(label='Deleted data:', stats=stats),
|
||||||
str(cache),
|
str(cache),
|
||||||
DASHES)
|
DASHES, logger=logging.getLogger('borg.output.stats'))
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
|
|
||||||
def do_upgrade(self, args):
|
def do_upgrade(self, args):
|
||||||
|
@ -1164,7 +1168,48 @@ class Archiver:
|
||||||
init_epilog = textwrap.dedent("""
|
init_epilog = textwrap.dedent("""
|
||||||
This command initializes an empty repository. A repository is a filesystem
|
This command initializes an empty repository. A repository is a filesystem
|
||||||
directory containing the deduplicated data from zero or more archives.
|
directory containing the deduplicated data from zero or more archives.
|
||||||
Encryption can be enabled at repository init time.
|
|
||||||
|
Encryption can be enabled at repository init time (the default).
|
||||||
|
|
||||||
|
It is not recommended to disable encryption. Repository encryption protects you
|
||||||
|
e.g. against the case that an attacker has access to your backup repository.
|
||||||
|
|
||||||
|
But be careful with the key / the passphrase:
|
||||||
|
|
||||||
|
If you want "passphrase-only" security, use the repokey mode. The key will
|
||||||
|
be stored inside the repository (in its "config" file). In above mentioned
|
||||||
|
attack scenario, the attacker will have the key (but not the passphrase).
|
||||||
|
|
||||||
|
If you want "passphrase and having-the-key" security, use the keyfile mode.
|
||||||
|
The key will be stored in your home directory (in .config/borg/keys). In
|
||||||
|
the attack scenario, the attacker who has just access to your repo won't have
|
||||||
|
the key (and also not the passphrase).
|
||||||
|
|
||||||
|
Make a backup copy of the key file (keyfile mode) or repo config file
|
||||||
|
(repokey mode) and keep it at a safe place, so you still have the key in
|
||||||
|
case it gets corrupted or lost. Also keep the passphrase at a safe place.
|
||||||
|
The backup that is encrypted with that key won't help you with that, of course.
|
||||||
|
|
||||||
|
Make sure you use a good passphrase. Not too short, not too simple. The real
|
||||||
|
encryption / decryption key is encrypted with / locked by your passphrase.
|
||||||
|
If an attacker gets your key, he can't unlock and use it without knowing the
|
||||||
|
passphrase.
|
||||||
|
|
||||||
|
Be careful with special or non-ascii characters in your passphrase:
|
||||||
|
|
||||||
|
- Borg processes the passphrase as unicode (and encodes it as utf-8),
|
||||||
|
so it does not have problems dealing with even the strangest characters.
|
||||||
|
- BUT: that does not necessarily apply to your OS / VM / keyboard configuration.
|
||||||
|
|
||||||
|
So better use a long passphrase made from simple ascii chars than one that
|
||||||
|
includes non-ascii stuff or characters that are hard/impossible to enter on
|
||||||
|
a different keyboard layout.
|
||||||
|
|
||||||
|
You can change your passphrase for existing repos at any time, it won't affect
|
||||||
|
the encryption/decryption key or other secrets.
|
||||||
|
|
||||||
|
When encrypting, AES-CTR-256 is used for encryption, and HMAC-SHA256 for
|
||||||
|
authentication. Hardware acceleration will be used automatically.
|
||||||
""")
|
""")
|
||||||
subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False,
|
subparser = subparsers.add_parser('init', parents=[common_parser], add_help=False,
|
||||||
description=self.do_init.__doc__, epilog=init_epilog,
|
description=self.do_init.__doc__, epilog=init_epilog,
|
||||||
|
@ -1213,6 +1258,18 @@ class Archiver:
|
||||||
required).
|
required).
|
||||||
- The archive checks can be time consuming, they can be skipped using the
|
- The archive checks can be time consuming, they can be skipped using the
|
||||||
--repository-only option.
|
--repository-only option.
|
||||||
|
|
||||||
|
The --verify-data option will perform a full integrity verification (as opposed to
|
||||||
|
checking the CRC32 of the segment) of data, which means reading the data from the
|
||||||
|
repository, decrypting and decompressing it. This is a cryptographic verification,
|
||||||
|
which will detect (accidental) corruption. For encrypted repositories it is
|
||||||
|
tamper-resistant as well, unless the attacker has access to the keys.
|
||||||
|
|
||||||
|
It is also very slow.
|
||||||
|
|
||||||
|
--verify-data only verifies data used by the archives specified with --last,
|
||||||
|
--prefix or an explicitly named archive. If none of these are passed,
|
||||||
|
all data in the repository is verified.
|
||||||
""")
|
""")
|
||||||
subparser = subparsers.add_parser('check', parents=[common_parser], add_help=False,
|
subparser = subparsers.add_parser('check', parents=[common_parser], add_help=False,
|
||||||
description=self.do_check.__doc__,
|
description=self.do_check.__doc__,
|
||||||
|
@ -1229,6 +1286,10 @@ class Archiver:
|
||||||
subparser.add_argument('--archives-only', dest='archives_only', action='store_true',
|
subparser.add_argument('--archives-only', dest='archives_only', action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='only perform archives checks')
|
help='only perform archives checks')
|
||||||
|
subparser.add_argument('--verify-data', dest='verify_data', action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='perform cryptographic archive data integrity verification '
|
||||||
|
'(conflicts with --repository-only)')
|
||||||
subparser.add_argument('--repair', dest='repair', action='store_true',
|
subparser.add_argument('--repair', dest='repair', action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help='attempt to repair any inconsistencies found')
|
help='attempt to repair any inconsistencies found')
|
||||||
|
@ -1240,6 +1301,9 @@ class Archiver:
|
||||||
help='only check last N archives (Default: all)')
|
help='only check last N archives (Default: all)')
|
||||||
subparser.add_argument('-P', '--prefix', dest='prefix', type=str,
|
subparser.add_argument('-P', '--prefix', dest='prefix', type=str,
|
||||||
help='only consider archive names starting with this prefix')
|
help='only consider archive names starting with this prefix')
|
||||||
|
subparser.add_argument('-p', '--progress', dest='progress',
|
||||||
|
action='store_true', default=False,
|
||||||
|
help="""show progress display while checking""")
|
||||||
|
|
||||||
change_passphrase_epilog = textwrap.dedent("""
|
change_passphrase_epilog = textwrap.dedent("""
|
||||||
The key files used for repository encryption are optionally passphrase
|
The key files used for repository encryption are optionally passphrase
|
||||||
|
@ -1401,6 +1465,10 @@ class Archiver:
|
||||||
be restricted by using the ``--exclude`` option.
|
be restricted by using the ``--exclude`` option.
|
||||||
|
|
||||||
See the output of the "borg help patterns" command for more help on exclude patterns.
|
See the output of the "borg help patterns" command for more help on exclude patterns.
|
||||||
|
|
||||||
|
By using ``--dry-run``, you can do all extraction steps except actually writing the
|
||||||
|
output data: reading metadata and data chunks from the repo, checking the hash/hmac,
|
||||||
|
decrypting, decompressing.
|
||||||
""")
|
""")
|
||||||
subparser = subparsers.add_parser('extract', parents=[common_parser], add_help=False,
|
subparser = subparsers.add_parser('extract', parents=[common_parser], add_help=False,
|
||||||
description=self.do_extract.__doc__,
|
description=self.do_extract.__doc__,
|
||||||
|
@ -2002,12 +2070,27 @@ class Archiver:
|
||||||
check_extension_modules()
|
check_extension_modules()
|
||||||
selftest(logger)
|
selftest(logger)
|
||||||
|
|
||||||
|
def _setup_implied_logging(self, args):
|
||||||
|
""" turn on INFO level logging for args that imply that they will produce output """
|
||||||
|
# map of option name to name of logger for that option
|
||||||
|
option_logger = {
|
||||||
|
'output_list': 'borg.output.list',
|
||||||
|
'show_version': 'borg.output.show-version',
|
||||||
|
'show_rc': 'borg.output.show-rc',
|
||||||
|
'stats': 'borg.output.stats',
|
||||||
|
'progress': 'borg.output.progress',
|
||||||
|
}
|
||||||
|
for option, logger_name in option_logger.items():
|
||||||
|
if args.get(option, False):
|
||||||
|
logging.getLogger(logger_name).setLevel('INFO')
|
||||||
|
|
||||||
def run(self, args):
|
def run(self, args):
|
||||||
os.umask(args.umask) # early, before opening files
|
os.umask(args.umask) # early, before opening files
|
||||||
self.lock_wait = args.lock_wait
|
self.lock_wait = args.lock_wait
|
||||||
setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this!
|
setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this!
|
||||||
|
self._setup_implied_logging(vars(args))
|
||||||
if args.show_version:
|
if args.show_version:
|
||||||
logger.info('borgbackup version %s' % __version__)
|
logging.getLogger('borg.output.show-version').info('borgbackup version %s' % __version__)
|
||||||
self.prerun_checks(logger)
|
self.prerun_checks(logger)
|
||||||
if is_slow_msgpack():
|
if is_slow_msgpack():
|
||||||
logger.warning("Using a pure-python msgpack! This will result in lower performance.")
|
logger.warning("Using a pure-python msgpack! This will result in lower performance.")
|
||||||
|
@ -2037,6 +2120,14 @@ def sig_info_handler(signum, stack): # pragma: no cover
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class SIGTERMReceived(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def sig_term_handler(signum, stack):
|
||||||
|
raise SIGTERMReceived
|
||||||
|
|
||||||
|
|
||||||
def setup_signal_handlers(): # pragma: no cover
|
def setup_signal_handlers(): # pragma: no cover
|
||||||
sigs = []
|
sigs = []
|
||||||
if hasattr(signal, 'SIGUSR1'):
|
if hasattr(signal, 'SIGUSR1'):
|
||||||
|
@ -2045,6 +2136,7 @@ def setup_signal_handlers(): # pragma: no cover
|
||||||
sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t)
|
sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t)
|
||||||
for sig in sigs:
|
for sig in sigs:
|
||||||
signal.signal(sig, sig_info_handler)
|
signal.signal(sig, sig_info_handler)
|
||||||
|
signal.signal(signal.SIGTERM, sig_term_handler)
|
||||||
|
|
||||||
|
|
||||||
def main(): # pragma: no cover
|
def main(): # pragma: no cover
|
||||||
|
@ -2076,18 +2168,22 @@ def main(): # pragma: no cover
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
msg = 'Keyboard interrupt.\n%s\n%s' % (traceback.format_exc(), sysinfo())
|
msg = 'Keyboard interrupt.\n%s\n%s' % (traceback.format_exc(), sysinfo())
|
||||||
exit_code = EXIT_ERROR
|
exit_code = EXIT_ERROR
|
||||||
|
except SIGTERMReceived:
|
||||||
|
msg = 'Received SIGTERM.'
|
||||||
|
exit_code = EXIT_ERROR
|
||||||
if msg:
|
if msg:
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
if args.show_rc:
|
if args.show_rc:
|
||||||
|
rc_logger = logging.getLogger('borg.output.show-rc')
|
||||||
exit_msg = 'terminating with %s status, rc %d'
|
exit_msg = 'terminating with %s status, rc %d'
|
||||||
if exit_code == EXIT_SUCCESS:
|
if exit_code == EXIT_SUCCESS:
|
||||||
logger.info(exit_msg % ('success', exit_code))
|
rc_logger.info(exit_msg % ('success', exit_code))
|
||||||
elif exit_code == EXIT_WARNING:
|
elif exit_code == EXIT_WARNING:
|
||||||
logger.warning(exit_msg % ('warning', exit_code))
|
rc_logger.warning(exit_msg % ('warning', exit_code))
|
||||||
elif exit_code == EXIT_ERROR:
|
elif exit_code == EXIT_ERROR:
|
||||||
logger.error(exit_msg % ('error', exit_code))
|
rc_logger.error(exit_msg % ('error', exit_code))
|
||||||
else:
|
else:
|
||||||
logger.error(exit_msg % ('abnormal', exit_code or 666))
|
rc_logger.error(exit_msg % ('abnormal', exit_code or 666))
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,14 @@ UMASK_DEFAULT = 0o077
|
||||||
CACHE_TAG_NAME = 'CACHEDIR.TAG'
|
CACHE_TAG_NAME = 'CACHEDIR.TAG'
|
||||||
CACHE_TAG_CONTENTS = b'Signature: 8a477f597d28d172789f06886806bc55'
|
CACHE_TAG_CONTENTS = b'Signature: 8a477f597d28d172789f06886806bc55'
|
||||||
|
|
||||||
DEFAULT_MAX_SEGMENT_SIZE = 5 * 1024 * 1024
|
# A large, but not unreasonably large segment size. Always less than 2 GiB (for legacy file systems). We choose
|
||||||
DEFAULT_SEGMENTS_PER_DIR = 10000
|
# 500 MiB which means that no indirection from the inode is needed for typical Linux file systems.
|
||||||
|
# Note that this is a soft-limit and can be exceeded (worst case) by a full maximum chunk size and some metadata
|
||||||
|
# bytes. That's why it's 500 MiB instead of 512 MiB.
|
||||||
|
DEFAULT_MAX_SEGMENT_SIZE = 500 * 1024 * 1024
|
||||||
|
|
||||||
|
# A few hundred files per directory to go easy on filesystems which don't like too many files per dir (NTFS)
|
||||||
|
DEFAULT_SEGMENTS_PER_DIR = 500
|
||||||
|
|
||||||
CHUNK_MIN_EXP = 19 # 2**19 == 512kiB
|
CHUNK_MIN_EXP = 19 # 2**19 == 512kiB
|
||||||
CHUNK_MAX_EXP = 23 # 2**23 == 8MiB
|
CHUNK_MAX_EXP = 23 # 2**23 == 8MiB
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
"""A thin OpenSSL wrapper
|
"""A thin OpenSSL wrapper"""
|
||||||
|
|
||||||
This could be replaced by PyCrypto maybe?
|
|
||||||
"""
|
|
||||||
from libc.stdlib cimport malloc, free
|
from libc.stdlib cimport malloc, free
|
||||||
from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release
|
from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release
|
||||||
|
|
||||||
API_VERSION = 3
|
API_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
cdef extern from "openssl/rand.h":
|
|
||||||
int RAND_bytes(unsigned char *buf, int num)
|
|
||||||
|
|
||||||
|
|
||||||
cdef extern from "openssl/evp.h":
|
cdef extern from "openssl/evp.h":
|
||||||
ctypedef struct EVP_MD:
|
ctypedef struct EVP_MD:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -11,7 +11,6 @@ import stat
|
||||||
import textwrap
|
import textwrap
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
from shutil import get_terminal_size
|
|
||||||
import sys
|
import sys
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
import platform
|
import platform
|
||||||
|
@ -82,7 +81,7 @@ def check_extension_modules():
|
||||||
raise ExtensionModuleError
|
raise ExtensionModuleError
|
||||||
if crypto.API_VERSION != 3:
|
if crypto.API_VERSION != 3:
|
||||||
raise ExtensionModuleError
|
raise ExtensionModuleError
|
||||||
if platform.API_VERSION != 2:
|
if platform.API_VERSION != 3:
|
||||||
raise ExtensionModuleError
|
raise ExtensionModuleError
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,57 +171,6 @@ def prune_split(archives, pattern, n, skip=[]):
|
||||||
return keep
|
return keep
|
||||||
|
|
||||||
|
|
||||||
class Statistics:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.osize = self.csize = self.usize = self.nfiles = 0
|
|
||||||
self.last_progress = 0 # timestamp when last progress was shown
|
|
||||||
|
|
||||||
def update(self, size, csize, unique):
|
|
||||||
self.osize += size
|
|
||||||
self.csize += csize
|
|
||||||
if unique:
|
|
||||||
self.usize += csize
|
|
||||||
|
|
||||||
summary = """\
|
|
||||||
Original size Compressed size Deduplicated size
|
|
||||||
{label:15} {stats.osize_fmt:>20s} {stats.csize_fmt:>20s} {stats.usize_fmt:>20s}"""
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.summary.format(stats=self, label='This archive:')
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<{cls} object at {hash:#x} ({self.osize}, {self.csize}, {self.usize})>".format(cls=type(self).__name__, hash=id(self), self=self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def osize_fmt(self):
|
|
||||||
return format_file_size(self.osize)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def usize_fmt(self):
|
|
||||||
return format_file_size(self.usize)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def csize_fmt(self):
|
|
||||||
return format_file_size(self.csize)
|
|
||||||
|
|
||||||
def show_progress(self, item=None, final=False, stream=None, dt=None):
|
|
||||||
now = time.time()
|
|
||||||
if dt is None or now - self.last_progress > dt:
|
|
||||||
self.last_progress = now
|
|
||||||
columns, lines = get_terminal_size()
|
|
||||||
if not final:
|
|
||||||
msg = '{0.osize_fmt} O {0.csize_fmt} C {0.usize_fmt} D {0.nfiles} N '.format(self)
|
|
||||||
path = remove_surrogates(item[b'path']) if item else ''
|
|
||||||
space = columns - len(msg)
|
|
||||||
if space < len('...') + len(path):
|
|
||||||
path = '%s...%s' % (path[:(space // 2) - len('...')], path[-space // 2:])
|
|
||||||
msg += "{0:<{space}}".format(path, space=space)
|
|
||||||
else:
|
|
||||||
msg = ' ' * columns
|
|
||||||
print(msg, file=stream or sys.stderr, end="\r", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_home_dir():
|
def get_home_dir():
|
||||||
"""Get user's home directory while preferring a possibly set HOME
|
"""Get user's home directory while preferring a possibly set HOME
|
||||||
environment variable
|
environment variable
|
||||||
|
@ -282,8 +230,7 @@ def load_excludes(fh):
|
||||||
"""Load and parse exclude patterns from file object. Lines empty or starting with '#' after stripping whitespace on
|
"""Load and parse exclude patterns from file object. Lines empty or starting with '#' after stripping whitespace on
|
||||||
both line ends are ignored.
|
both line ends are ignored.
|
||||||
"""
|
"""
|
||||||
patterns = (line for line in (i.strip() for i in fh) if not line.startswith('#'))
|
return [parse_pattern(pattern) for pattern in clean_lines(fh)]
|
||||||
return [parse_pattern(pattern) for pattern in patterns if pattern]
|
|
||||||
|
|
||||||
|
|
||||||
def update_excludes(args):
|
def update_excludes(args):
|
||||||
|
@ -734,11 +681,15 @@ def posix_acl_use_stored_uid_gid(acl):
|
||||||
|
|
||||||
def safe_decode(s, coding='utf-8', errors='surrogateescape'):
|
def safe_decode(s, coding='utf-8', errors='surrogateescape'):
|
||||||
"""decode bytes to str, with round-tripping "invalid" bytes"""
|
"""decode bytes to str, with round-tripping "invalid" bytes"""
|
||||||
|
if s is None:
|
||||||
|
return None
|
||||||
return s.decode(coding, errors)
|
return s.decode(coding, errors)
|
||||||
|
|
||||||
|
|
||||||
def safe_encode(s, coding='utf-8', errors='surrogateescape'):
|
def safe_encode(s, coding='utf-8', errors='surrogateescape'):
|
||||||
"""encode str to bytes, with round-tripping "invalid" bytes"""
|
"""encode str to bytes, with round-tripping "invalid" bytes"""
|
||||||
|
if s is None:
|
||||||
|
return None
|
||||||
return s.encode(coding, errors)
|
return s.encode(coding, errors)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1038,7 +989,7 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None,
|
||||||
|
|
||||||
|
|
||||||
class ProgressIndicatorPercent:
|
class ProgressIndicatorPercent:
|
||||||
def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%", file=None):
|
def __init__(self, total, step=5, start=0, same_line=False, msg="%3.0f%%"):
|
||||||
"""
|
"""
|
||||||
Percentage-based progress indicator
|
Percentage-based progress indicator
|
||||||
|
|
||||||
|
@ -1047,17 +998,33 @@ class ProgressIndicatorPercent:
|
||||||
:param start: at which percent value to start
|
:param start: at which percent value to start
|
||||||
:param same_line: if True, emit output always on same line
|
:param same_line: if True, emit output always on same line
|
||||||
:param msg: output message, must contain one %f placeholder for the percentage
|
:param msg: output message, must contain one %f placeholder for the percentage
|
||||||
:param file: output file, default: sys.stderr
|
|
||||||
"""
|
"""
|
||||||
self.counter = 0 # 0 .. (total-1)
|
self.counter = 0 # 0 .. (total-1)
|
||||||
self.total = total
|
self.total = total
|
||||||
self.trigger_at = start # output next percentage value when reaching (at least) this
|
self.trigger_at = start # output next percentage value when reaching (at least) this
|
||||||
self.step = step
|
self.step = step
|
||||||
if file is None:
|
|
||||||
file = sys.stderr
|
|
||||||
self.file = file
|
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.same_line = same_line
|
self.same_line = same_line
|
||||||
|
self.handler = None
|
||||||
|
self.logger = logging.getLogger('borg.output.progress')
|
||||||
|
|
||||||
|
# If there are no handlers, set one up explicitly because the
|
||||||
|
# terminator and propagation needs to be set. If there are,
|
||||||
|
# they must have been set up by BORG_LOGGING_CONF: skip setup.
|
||||||
|
if not self.logger.handlers:
|
||||||
|
self.handler = logging.StreamHandler(stream=sys.stderr)
|
||||||
|
self.handler.setLevel(logging.INFO)
|
||||||
|
self.handler.terminator = '\r' if self.same_line else '\n'
|
||||||
|
|
||||||
|
self.logger.addHandler(self.handler)
|
||||||
|
if self.logger.level == logging.NOTSET:
|
||||||
|
self.logger.setLevel(logging.WARN)
|
||||||
|
self.logger.propagate = False
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.handler is not None:
|
||||||
|
self.logger.removeHandler(self.handler)
|
||||||
|
self.handler.close()
|
||||||
|
|
||||||
def progress(self, current=None):
|
def progress(self, current=None):
|
||||||
if current is not None:
|
if current is not None:
|
||||||
|
@ -1074,11 +1041,11 @@ class ProgressIndicatorPercent:
|
||||||
return self.output(pct)
|
return self.output(pct)
|
||||||
|
|
||||||
def output(self, percent):
|
def output(self, percent):
|
||||||
print(self.msg % percent, file=self.file, end='\r' if self.same_line else '\n', flush=True)
|
self.logger.info(self.msg % percent)
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
if self.same_line:
|
if self.same_line:
|
||||||
print(" " * len(self.msg % 100.0), file=self.file, end='\r')
|
self.logger.info(" " * len(self.msg % 100.0))
|
||||||
|
|
||||||
|
|
||||||
class ProgressIndicatorEndless:
|
class ProgressIndicatorEndless:
|
||||||
|
@ -1128,7 +1095,7 @@ def sysinfo():
|
||||||
return '\n'.join(info)
|
return '\n'.join(info)
|
||||||
|
|
||||||
|
|
||||||
def log_multi(*msgs, level=logging.INFO):
|
def log_multi(*msgs, level=logging.INFO, logger=logger):
|
||||||
"""
|
"""
|
||||||
log multiple lines of text, each line by a separate logging call for cosmetic reasons
|
log multiple lines of text, each line by a separate logging call for cosmetic reasons
|
||||||
|
|
||||||
|
@ -1167,7 +1134,7 @@ class ItemFormatter:
|
||||||
'NUL': 'NUL character for creating print0 / xargs -0 like ouput, see bpath',
|
'NUL': 'NUL character for creating print0 / xargs -0 like ouput, see bpath',
|
||||||
}
|
}
|
||||||
KEY_GROUPS = (
|
KEY_GROUPS = (
|
||||||
('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget'),
|
('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'flags'),
|
||||||
('size', 'csize', 'num_chunks', 'unique_chunks'),
|
('size', 'csize', 'num_chunks', 'unique_chunks'),
|
||||||
('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'),
|
('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'),
|
||||||
tuple(sorted(hashlib.algorithms_guaranteed)),
|
tuple(sorted(hashlib.algorithms_guaranteed)),
|
||||||
|
@ -1260,6 +1227,7 @@ class ItemFormatter:
|
||||||
item_data['source'] = source
|
item_data['source'] = source
|
||||||
item_data['linktarget'] = source
|
item_data['linktarget'] = source
|
||||||
item_data['extra'] = extra
|
item_data['extra'] = extra
|
||||||
|
item_data['flags'] = item.get(b'bsdflags')
|
||||||
for key in self.used_call_keys:
|
for key in self.used_call_keys:
|
||||||
item_data[key] = self.call_keys[key](item)
|
item_data[key] = self.call_keys[key](item)
|
||||||
return item_data
|
return item_data
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
from .constants import ITEM_KEYS
|
||||||
|
from .helpers import safe_encode, safe_decode, bigint_to_int, int_to_bigint, StableDict
|
||||||
|
|
||||||
|
|
||||||
|
class PropDict:
|
||||||
|
"""
|
||||||
|
Manage a dictionary via properties.
|
||||||
|
|
||||||
|
- initialization by giving a dict or kw args
|
||||||
|
- on initialization, normalize dict keys to be str type
|
||||||
|
- access dict via properties, like: x.key_name
|
||||||
|
- membership check via: 'key_name' in x
|
||||||
|
- optionally, encode when setting a value
|
||||||
|
- optionally, decode when getting a value
|
||||||
|
- be safe against typos in key names: check against VALID_KEYS
|
||||||
|
- when setting a value: check type of value
|
||||||
|
"""
|
||||||
|
VALID_KEYS = None # override with <set of str> in child class
|
||||||
|
|
||||||
|
__slots__ = ("_dict", ) # avoid setting attributes not supported by properties
|
||||||
|
|
||||||
|
def __init__(self, data_dict=None, **kw):
|
||||||
|
if data_dict is None:
|
||||||
|
data = kw
|
||||||
|
elif not isinstance(data_dict, dict):
|
||||||
|
raise TypeError("data_dict must be dict")
|
||||||
|
else:
|
||||||
|
data = data_dict
|
||||||
|
# internally, we want an dict with only str-typed keys
|
||||||
|
_dict = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if isinstance(k, bytes):
|
||||||
|
k = k.decode()
|
||||||
|
elif not isinstance(k, str):
|
||||||
|
raise TypeError("dict keys must be str or bytes, not %r" % k)
|
||||||
|
_dict[k] = v
|
||||||
|
unknown_keys = set(_dict) - self.VALID_KEYS
|
||||||
|
if unknown_keys:
|
||||||
|
raise ValueError("dict contains unknown keys %s" % ','.join(unknown_keys))
|
||||||
|
self._dict = _dict
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""return the internal dictionary"""
|
||||||
|
return StableDict(self._dict)
|
||||||
|
|
||||||
|
def _check_key(self, key):
|
||||||
|
"""make sure key is of type str and known"""
|
||||||
|
if not isinstance(key, str):
|
||||||
|
raise TypeError("key must be str")
|
||||||
|
if key not in self.VALID_KEYS:
|
||||||
|
raise ValueError("key '%s' is not a valid key" % key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
"""do we have this key?"""
|
||||||
|
return self._check_key(key) in self._dict
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
"""get value for key, return default if key does not exist"""
|
||||||
|
return getattr(self, self._check_key(key), default)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_property(key, value_type, value_type_name=None, encode=None, decode=None):
|
||||||
|
"""return a property that deals with self._dict[key]"""
|
||||||
|
assert isinstance(key, str)
|
||||||
|
if value_type_name is None:
|
||||||
|
value_type_name = value_type.__name__
|
||||||
|
doc = "%s (%s)" % (key, value_type_name)
|
||||||
|
type_error_msg = "%s value must be %s" % (key, value_type_name)
|
||||||
|
attr_error_msg = "attribute %s not found" % key
|
||||||
|
|
||||||
|
def _get(self):
|
||||||
|
try:
|
||||||
|
value = self._dict[key]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(attr_error_msg) from None
|
||||||
|
if decode is not None:
|
||||||
|
value = decode(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _set(self, value):
|
||||||
|
if not isinstance(value, value_type):
|
||||||
|
raise TypeError(type_error_msg)
|
||||||
|
if encode is not None:
|
||||||
|
value = encode(value)
|
||||||
|
self._dict[key] = value
|
||||||
|
|
||||||
|
def _del(self):
|
||||||
|
try:
|
||||||
|
del self._dict[key]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(attr_error_msg) from None
|
||||||
|
|
||||||
|
return property(_get, _set, _del, doc=doc)
|
||||||
|
|
||||||
|
|
||||||
|
class Item(PropDict):
|
||||||
|
"""
|
||||||
|
Item abstraction that deals with validation and the low-level details internally:
|
||||||
|
|
||||||
|
Items are created either from msgpack unpacker output, from another dict, from kwargs or
|
||||||
|
built step-by-step by setting attributes.
|
||||||
|
|
||||||
|
msgpack gives us a dict with bytes-typed keys, just give it to Item(d) and use item.key_name later.
|
||||||
|
msgpack gives us byte-typed values for stuff that should be str, we automatically decode when getting
|
||||||
|
such a property and encode when setting it.
|
||||||
|
|
||||||
|
If an Item shall be serialized, give as_dict() method output to msgpack packer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
VALID_KEYS = set(key.decode() for key in ITEM_KEYS) # we want str-typed keys
|
||||||
|
|
||||||
|
__slots__ = ("_dict", ) # avoid setting attributes not supported by properties
|
||||||
|
|
||||||
|
# properties statically defined, so that IDEs can know their names:
|
||||||
|
|
||||||
|
path = PropDict._make_property('path', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
source = PropDict._make_property('source', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
acl_access = PropDict._make_property('acl_access', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
acl_default = PropDict._make_property('acl_default', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
acl_extended = PropDict._make_property('acl_extended', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
acl_nfs4 = PropDict._make_property('acl_nfs4', str, 'surrogate-escaped str', encode=safe_encode, decode=safe_decode)
|
||||||
|
|
||||||
|
user = PropDict._make_property('user', (str, type(None)), 'surrogate-escaped str or None', encode=safe_encode, decode=safe_decode)
|
||||||
|
group = PropDict._make_property('group', (str, type(None)), 'surrogate-escaped str or None', encode=safe_encode, decode=safe_decode)
|
||||||
|
|
||||||
|
mode = PropDict._make_property('mode', int)
|
||||||
|
uid = PropDict._make_property('uid', int)
|
||||||
|
gid = PropDict._make_property('gid', int)
|
||||||
|
rdev = PropDict._make_property('rdev', int)
|
||||||
|
bsdflags = PropDict._make_property('bsdflags', int)
|
||||||
|
|
||||||
|
atime = PropDict._make_property('atime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
|
||||||
|
ctime = PropDict._make_property('ctime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
|
||||||
|
mtime = PropDict._make_property('mtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
|
||||||
|
|
||||||
|
hardlink_master = PropDict._make_property('hardlink_master', bool)
|
||||||
|
|
||||||
|
chunks = PropDict._make_property('chunks', list)
|
||||||
|
|
||||||
|
xattrs = PropDict._make_property('xattrs', StableDict)
|
|
@ -1,4 +1,4 @@
|
||||||
from binascii import a2b_base64, b2a_base64
|
from binascii import a2b_base64, b2a_base64, hexlify
|
||||||
import configparser
|
import configparser
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
|
@ -413,16 +413,19 @@ class KeyfileKey(KeyfileKeyBase):
|
||||||
FILE_ID = 'BORG_KEY'
|
FILE_ID = 'BORG_KEY'
|
||||||
|
|
||||||
def sanity_check(self, filename, id):
|
def sanity_check(self, filename, id):
|
||||||
with open(filename, 'r') as fd:
|
file_id = self.FILE_ID.encode() + b' '
|
||||||
line = fd.readline().strip()
|
repo_id = hexlify(id)
|
||||||
if not line.startswith(self.FILE_ID):
|
with open(filename, 'rb') as fd:
|
||||||
|
# we do the magic / id check in binary mode to avoid stumbling over
|
||||||
|
# decoding errors if somebody has binary files in the keys dir for some reason.
|
||||||
|
if fd.read(len(file_id)) != file_id:
|
||||||
raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
|
raise KeyfileInvalidError(self.repository._location.canonical_path(), filename)
|
||||||
if line[len(self.FILE_ID) + 1:] != id:
|
if fd.read(len(repo_id)) != repo_id:
|
||||||
raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)
|
raise KeyfileMismatchError(self.repository._location.canonical_path(), filename)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
def find_key(self):
|
def find_key(self):
|
||||||
id = self.repository.id_str
|
id = self.repository.id
|
||||||
keyfile = os.environ.get('BORG_KEY_FILE')
|
keyfile = os.environ.get('BORG_KEY_FILE')
|
||||||
if keyfile:
|
if keyfile:
|
||||||
return self.sanity_check(keyfile, id)
|
return self.sanity_check(keyfile, id)
|
||||||
|
|
|
@ -88,7 +88,7 @@ def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', lev
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
handler = logging.StreamHandler(stream)
|
handler = logging.StreamHandler(stream)
|
||||||
if is_serve:
|
if is_serve:
|
||||||
fmt = '$LOG %(levelname)s Remote: %(message)s'
|
fmt = '$LOG %(levelname)s %(name)s Remote: %(message)s'
|
||||||
else:
|
else:
|
||||||
fmt = '%(message)s'
|
fmt = '%(message)s'
|
||||||
handler.setFormatter(logging.Formatter(fmt))
|
handler.setFormatter(logging.Formatter(fmt))
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from .platform_base import acl_get, acl_set, SyncFile, sync_dir, set_flags, get_flags, swidth, API_VERSION
|
||||||
|
|
||||||
if sys.platform.startswith('linux'): # pragma: linux only
|
if sys.platform.startswith('linux'): # pragma: linux only
|
||||||
from .platform_linux import acl_get, acl_set, API_VERSION
|
from .platform_linux import acl_get, acl_set, SyncFile, set_flags, get_flags, swidth, API_VERSION
|
||||||
elif sys.platform.startswith('freebsd'): # pragma: freebsd only
|
elif sys.platform.startswith('freebsd'): # pragma: freebsd only
|
||||||
from .platform_freebsd import acl_get, acl_set, API_VERSION
|
from .platform_freebsd import acl_get, acl_set, swidth, API_VERSION
|
||||||
elif sys.platform == 'darwin': # pragma: darwin only
|
elif sys.platform == 'darwin': # pragma: darwin only
|
||||||
from .platform_darwin import acl_get, acl_set, API_VERSION
|
from .platform_darwin import acl_get, acl_set, swidth, API_VERSION
|
||||||
else: # pragma: unknown platform only
|
|
||||||
API_VERSION = 2
|
|
||||||
|
|
||||||
def acl_get(path, item, st, numeric_owner=False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def acl_set(path, item, numeric_owner=False):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
API_VERSION = 3
|
||||||
|
|
||||||
|
fdatasync = getattr(os, 'fdatasync', os.fsync)
|
||||||
|
|
||||||
|
|
||||||
|
def acl_get(path, item, st, numeric_owner=False):
|
||||||
|
"""
|
||||||
|
Saves ACL Entries
|
||||||
|
|
||||||
|
If `numeric_owner` is True the user/group field is not preserved only uid/gid
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def acl_set(path, item, numeric_owner=False):
|
||||||
|
"""
|
||||||
|
Restore ACL Entries
|
||||||
|
|
||||||
|
If `numeric_owner` is True the stored uid/gid is used instead
|
||||||
|
of the user/group names
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from os import lchflags
|
||||||
|
|
||||||
|
def set_flags(path, bsd_flags, fd=None):
|
||||||
|
lchflags(path, bsd_flags)
|
||||||
|
except ImportError:
|
||||||
|
def set_flags(path, bsd_flags, fd=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_flags(path, st):
|
||||||
|
"""Return BSD-style file flags for path or stat without following symlinks."""
|
||||||
|
return getattr(st, 'st_flags', 0)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_dir(path):
|
||||||
|
fd = os.open(path, os.O_RDONLY)
|
||||||
|
try:
|
||||||
|
os.fsync(fd)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncFile:
|
||||||
|
"""
|
||||||
|
A file class that is supposed to enable write ordering (one way or another) and data durability after close().
|
||||||
|
|
||||||
|
The degree to which either is possible varies with operating system, file system and hardware.
|
||||||
|
|
||||||
|
This fallback implements a naive and slow way of doing this. On some operating systems it can't actually
|
||||||
|
guarantee any of the above, since fsync() doesn't guarantee it. Furthermore it may not be possible at all
|
||||||
|
to satisfy the above guarantees on some hardware or operating systems. In these cases we hope that the thorough
|
||||||
|
checksumming implemented catches any corrupted data due to misordered, delayed or partial writes.
|
||||||
|
|
||||||
|
Note that POSIX doesn't specify *anything* about power failures (or similar failures). A system that
|
||||||
|
routinely loses files or corrupts file on power loss is POSIX compliant.
|
||||||
|
|
||||||
|
TODO: Use F_FULLSYNC on OSX.
|
||||||
|
TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.fd = open(path, 'wb')
|
||||||
|
self.fileno = self.fd.fileno()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.fd.write(data)
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
"""
|
||||||
|
Synchronize file contents. Everything written prior to sync() must become durable before anything written
|
||||||
|
after sync().
|
||||||
|
"""
|
||||||
|
self.fd.flush()
|
||||||
|
fdatasync(self.fileno)
|
||||||
|
if hasattr(os, 'posix_fadvise'):
|
||||||
|
os.posix_fadvise(self.fileno, 0, 0, os.POSIX_FADV_DONTNEED)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""sync() and close."""
|
||||||
|
self.sync()
|
||||||
|
self.fd.close()
|
||||||
|
sync_dir(os.path.dirname(self.fd.name))
|
||||||
|
|
||||||
|
|
||||||
|
def swidth(s):
|
||||||
|
"""terminal output width of string <s>
|
||||||
|
|
||||||
|
For western scripts, this is just len(s), but for cjk glyphs, 2 cells are used.
|
||||||
|
"""
|
||||||
|
return len(s)
|
|
@ -1,7 +1,8 @@
|
||||||
import os
|
import os
|
||||||
from .helpers import user2uid, group2gid, safe_decode, safe_encode
|
from .helpers import user2uid, group2gid, safe_decode, safe_encode
|
||||||
|
from .platform_posix import swidth
|
||||||
|
|
||||||
API_VERSION = 2
|
API_VERSION = 3
|
||||||
|
|
||||||
cdef extern from "sys/acl.h":
|
cdef extern from "sys/acl.h":
|
||||||
ctypedef struct _acl_t:
|
ctypedef struct _acl_t:
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import os
|
import os
|
||||||
from .helpers import posix_acl_use_stored_uid_gid, safe_encode, safe_decode
|
from .helpers import posix_acl_use_stored_uid_gid, safe_encode, safe_decode
|
||||||
|
from .platform_posix import swidth
|
||||||
|
|
||||||
API_VERSION = 2
|
API_VERSION = 3
|
||||||
|
|
||||||
cdef extern from "errno.h":
|
cdef extern from "errno.h":
|
||||||
int errno
|
int errno
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from stat import S_ISLNK
|
import resource
|
||||||
from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode
|
import stat
|
||||||
|
|
||||||
API_VERSION = 2
|
from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode
|
||||||
|
from .platform_base import SyncFile as BaseSyncFile
|
||||||
|
from .platform_posix import swidth
|
||||||
|
|
||||||
|
from libc cimport errno
|
||||||
|
|
||||||
|
API_VERSION = 3
|
||||||
|
|
||||||
cdef extern from "sys/types.h":
|
cdef extern from "sys/types.h":
|
||||||
int ACL_TYPE_ACCESS
|
int ACL_TYPE_ACCESS
|
||||||
int ACL_TYPE_DEFAULT
|
int ACL_TYPE_DEFAULT
|
||||||
|
ctypedef off64_t
|
||||||
|
|
||||||
cdef extern from "sys/acl.h":
|
cdef extern from "sys/acl.h":
|
||||||
ctypedef struct _acl_t:
|
ctypedef struct _acl_t:
|
||||||
|
@ -23,10 +30,78 @@ cdef extern from "sys/acl.h":
|
||||||
cdef extern from "acl/libacl.h":
|
cdef extern from "acl/libacl.h":
|
||||||
int acl_extended_file(const char *path)
|
int acl_extended_file(const char *path)
|
||||||
|
|
||||||
|
cdef extern from "fcntl.h":
|
||||||
|
int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags)
|
||||||
|
unsigned int SYNC_FILE_RANGE_WRITE
|
||||||
|
unsigned int SYNC_FILE_RANGE_WAIT_BEFORE
|
||||||
|
unsigned int SYNC_FILE_RANGE_WAIT_AFTER
|
||||||
|
|
||||||
|
cdef extern from "linux/fs.h":
|
||||||
|
# ioctls
|
||||||
|
int FS_IOC_SETFLAGS
|
||||||
|
int FS_IOC_GETFLAGS
|
||||||
|
|
||||||
|
# inode flags
|
||||||
|
int FS_NODUMP_FL
|
||||||
|
int FS_IMMUTABLE_FL
|
||||||
|
int FS_APPEND_FL
|
||||||
|
int FS_COMPR_FL
|
||||||
|
|
||||||
|
cdef extern from "stropts.h":
|
||||||
|
int ioctl(int fildes, int request, ...)
|
||||||
|
|
||||||
|
cdef extern from "errno.h":
|
||||||
|
int errno
|
||||||
|
|
||||||
|
cdef extern from "string.h":
|
||||||
|
char *strerror(int errnum)
|
||||||
|
|
||||||
_comment_re = re.compile(' *#.*', re.M)
|
_comment_re = re.compile(' *#.*', re.M)
|
||||||
|
|
||||||
|
|
||||||
|
BSD_TO_LINUX_FLAGS = {
|
||||||
|
stat.UF_NODUMP: FS_NODUMP_FL,
|
||||||
|
stat.UF_IMMUTABLE: FS_IMMUTABLE_FL,
|
||||||
|
stat.UF_APPEND: FS_APPEND_FL,
|
||||||
|
stat.UF_COMPRESSED: FS_COMPR_FL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_flags(path, bsd_flags, fd=None):
|
||||||
|
if fd is None and stat.S_ISLNK(os.lstat(path).st_mode):
|
||||||
|
return
|
||||||
|
cdef int flags = 0
|
||||||
|
for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():
|
||||||
|
if bsd_flags & bsd_flag:
|
||||||
|
flags |= linux_flag
|
||||||
|
open_fd = fd is None
|
||||||
|
if open_fd:
|
||||||
|
fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)
|
||||||
|
try:
|
||||||
|
if ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1:
|
||||||
|
raise OSError(errno, strerror(errno).decode(), path)
|
||||||
|
finally:
|
||||||
|
if open_fd:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_flags(path, st):
|
||||||
|
if stat.S_ISLNK(st.st_mode):
|
||||||
|
return 0
|
||||||
|
cdef int linux_flags
|
||||||
|
fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)
|
||||||
|
try:
|
||||||
|
if ioctl(fd, FS_IOC_GETFLAGS, &linux_flags) == -1:
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
bsd_flags = 0
|
||||||
|
for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():
|
||||||
|
if linux_flags & linux_flag:
|
||||||
|
bsd_flags |= bsd_flag
|
||||||
|
return bsd_flags
|
||||||
|
|
||||||
|
|
||||||
def acl_use_local_uid_gid(acl):
|
def acl_use_local_uid_gid(acl):
|
||||||
"""Replace the user/group field with the local uid/gid if possible
|
"""Replace the user/group field with the local uid/gid if possible
|
||||||
"""
|
"""
|
||||||
|
@ -77,17 +152,13 @@ cdef acl_numeric_ids(acl):
|
||||||
|
|
||||||
|
|
||||||
def acl_get(path, item, st, numeric_owner=False):
|
def acl_get(path, item, st, numeric_owner=False):
|
||||||
"""Saves ACL Entries
|
|
||||||
|
|
||||||
If `numeric_owner` is True the user/group field is not preserved only uid/gid
|
|
||||||
"""
|
|
||||||
cdef acl_t default_acl = NULL
|
cdef acl_t default_acl = NULL
|
||||||
cdef acl_t access_acl = NULL
|
cdef acl_t access_acl = NULL
|
||||||
cdef char *default_text = NULL
|
cdef char *default_text = NULL
|
||||||
cdef char *access_text = NULL
|
cdef char *access_text = NULL
|
||||||
|
|
||||||
p = <bytes>os.fsencode(path)
|
p = <bytes>os.fsencode(path)
|
||||||
if S_ISLNK(st.st_mode) or acl_extended_file(p) <= 0:
|
if stat.S_ISLNK(st.st_mode) or acl_extended_file(p) <= 0:
|
||||||
return
|
return
|
||||||
if numeric_owner:
|
if numeric_owner:
|
||||||
converter = acl_numeric_ids
|
converter = acl_numeric_ids
|
||||||
|
@ -112,11 +183,6 @@ def acl_get(path, item, st, numeric_owner=False):
|
||||||
|
|
||||||
|
|
||||||
def acl_set(path, item, numeric_owner=False):
|
def acl_set(path, item, numeric_owner=False):
|
||||||
"""Restore ACL Entries
|
|
||||||
|
|
||||||
If `numeric_owner` is True the stored uid/gid is used instead
|
|
||||||
of the user/group names
|
|
||||||
"""
|
|
||||||
cdef acl_t access_acl = NULL
|
cdef acl_t access_acl = NULL
|
||||||
cdef acl_t default_acl = NULL
|
cdef acl_t default_acl = NULL
|
||||||
|
|
||||||
|
@ -141,3 +207,45 @@ def acl_set(path, item, numeric_owner=False):
|
||||||
acl_set_file(p, ACL_TYPE_DEFAULT, default_acl)
|
acl_set_file(p, ACL_TYPE_DEFAULT, default_acl)
|
||||||
finally:
|
finally:
|
||||||
acl_free(default_acl)
|
acl_free(default_acl)
|
||||||
|
|
||||||
|
cdef _sync_file_range(fd, offset, length, flags):
|
||||||
|
assert offset & PAGE_MASK == 0, "offset %d not page-aligned" % offset
|
||||||
|
assert length & PAGE_MASK == 0, "length %d not page-aligned" % length
|
||||||
|
if sync_file_range(fd, offset, length, flags) != 0:
|
||||||
|
raise OSError(errno, os.strerror(errno))
|
||||||
|
os.posix_fadvise(fd, offset, length, os.POSIX_FADV_DONTNEED)
|
||||||
|
|
||||||
|
cdef unsigned PAGE_MASK = resource.getpagesize() - 1
|
||||||
|
|
||||||
|
|
||||||
|
class SyncFile(BaseSyncFile):
|
||||||
|
"""
|
||||||
|
Implemented using sync_file_range for asynchronous write-out and fdatasync for actual durability.
|
||||||
|
|
||||||
|
"write-out" means that dirty pages (= data that was written) are submitted to an I/O queue and will be send to
|
||||||
|
disk in the immediate future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
super().__init__(path)
|
||||||
|
self.offset = 0
|
||||||
|
self.write_window = (16 * 1024 ** 2) & ~PAGE_MASK
|
||||||
|
self.last_sync = 0
|
||||||
|
self.pending_sync = None
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.offset += self.fd.write(data)
|
||||||
|
offset = self.offset & ~PAGE_MASK
|
||||||
|
if offset >= self.last_sync + self.write_window:
|
||||||
|
self.fd.flush()
|
||||||
|
_sync_file_range(self.fileno, self.last_sync, offset - self.last_sync, SYNC_FILE_RANGE_WRITE)
|
||||||
|
if self.pending_sync is not None:
|
||||||
|
_sync_file_range(self.fileno, self.pending_sync, self.last_sync - self.pending_sync,
|
||||||
|
SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_BEFORE | SYNC_FILE_RANGE_WAIT_AFTER)
|
||||||
|
self.pending_sync = self.last_sync
|
||||||
|
self.last_sync = offset
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
self.fd.flush()
|
||||||
|
os.fdatasync(self.fileno)
|
||||||
|
os.posix_fadvise(self.fileno, 0, 0, os.POSIX_FADV_DONTNEED)
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
cdef extern from "wchar.h":
|
||||||
|
cdef int wcswidth(const Py_UNICODE *str, size_t n)
|
||||||
|
|
||||||
|
def swidth(s):
|
||||||
|
return wcswidth(s, len(s))
|
|
@ -301,7 +301,13 @@ class RemoteRepository:
|
||||||
if line.startswith('$LOG '):
|
if line.startswith('$LOG '):
|
||||||
_, level, msg = line.split(' ', 2)
|
_, level, msg = line.split(' ', 2)
|
||||||
level = getattr(logging, level, logging.CRITICAL) # str -> int
|
level = getattr(logging, level, logging.CRITICAL) # str -> int
|
||||||
logging.log(level, msg.rstrip())
|
if msg.startswith('Remote:'):
|
||||||
|
# server format: '$LOG <level> Remote: <msg>'
|
||||||
|
logging.log(level, msg.rstrip())
|
||||||
|
else:
|
||||||
|
# server format '$LOG <level> <logname> Remote: <msg>'
|
||||||
|
logname, msg = msg.split(' ', 1)
|
||||||
|
logging.getLogger(logname).log(level, msg.rstrip())
|
||||||
else:
|
else:
|
||||||
sys.stderr.write("Remote: " + line)
|
sys.stderr.write("Remote: " + line)
|
||||||
if w:
|
if w:
|
||||||
|
@ -418,6 +424,9 @@ class RepositoryCache(RepositoryNoCache):
|
||||||
|
|
||||||
Caches Repository GET operations using a local temporary Repository.
|
Caches Repository GET operations using a local temporary Repository.
|
||||||
"""
|
"""
|
||||||
|
# maximum object size that will be cached, 64 kiB.
|
||||||
|
THRESHOLD = 2**16
|
||||||
|
|
||||||
def __init__(self, repository):
|
def __init__(self, repository):
|
||||||
super().__init__(repository)
|
super().__init__(repository)
|
||||||
tmppath = tempfile.mkdtemp(prefix='borg-tmp')
|
tmppath = tempfile.mkdtemp(prefix='borg-tmp')
|
||||||
|
@ -438,7 +447,8 @@ class RepositoryCache(RepositoryNoCache):
|
||||||
except Repository.ObjectNotFound:
|
except Repository.ObjectNotFound:
|
||||||
for key_, data in repository_iterator:
|
for key_, data in repository_iterator:
|
||||||
if key_ == key:
|
if key_ == key:
|
||||||
self.caching_repo.put(key, data)
|
if len(data) <= self.THRESHOLD:
|
||||||
|
self.caching_repo.put(key, data)
|
||||||
yield data
|
yield data
|
||||||
break
|
break
|
||||||
# Consume any pending requests
|
# Consume any pending requests
|
||||||
|
|
|
@ -17,6 +17,7 @@ from .helpers import Error, ErrorWithTraceback, IntegrityError, Location, Progre
|
||||||
from .hashindex import NSIndex
|
from .hashindex import NSIndex
|
||||||
from .locking import UpgradableLock, LockError, LockErrorT
|
from .locking import UpgradableLock, LockError, LockErrorT
|
||||||
from .lrucache import LRUCache
|
from .lrucache import LRUCache
|
||||||
|
from .platform import SyncFile, sync_dir
|
||||||
|
|
||||||
MAX_OBJECT_SIZE = 20 * 1024 * 1024
|
MAX_OBJECT_SIZE = 20 * 1024 * 1024
|
||||||
MAGIC = b'BORG_SEG'
|
MAGIC = b'BORG_SEG'
|
||||||
|
@ -32,7 +33,7 @@ class Repository:
|
||||||
On disk layout:
|
On disk layout:
|
||||||
dir/README
|
dir/README
|
||||||
dir/config
|
dir/config
|
||||||
dir/data/<X / SEGMENTS_PER_DIR>/<X>
|
dir/data/<X // SEGMENTS_PER_DIR>/<X>
|
||||||
dir/index.X
|
dir/index.X
|
||||||
dir/hints.X
|
dir/hints.X
|
||||||
"""
|
"""
|
||||||
|
@ -507,7 +508,7 @@ class LoggedIO:
|
||||||
def __init__(self, path, limit, segments_per_dir, capacity=90):
|
def __init__(self, path, limit, segments_per_dir, capacity=90):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.fds = LRUCache(capacity,
|
self.fds = LRUCache(capacity,
|
||||||
dispose=lambda fd: fd.close())
|
dispose=self.close_fd)
|
||||||
self.segment = 0
|
self.segment = 0
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.segments_per_dir = segments_per_dir
|
self.segments_per_dir = segments_per_dir
|
||||||
|
@ -519,6 +520,11 @@ class LoggedIO:
|
||||||
self.fds.clear()
|
self.fds.clear()
|
||||||
self.fds = None # Just to make sure we're disabled
|
self.fds = None # Just to make sure we're disabled
|
||||||
|
|
||||||
|
def close_fd(self, fd):
|
||||||
|
if hasattr(os, 'posix_fadvise'): # only on UNIX
|
||||||
|
os.posix_fadvise(fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED)
|
||||||
|
fd.close()
|
||||||
|
|
||||||
def segment_iterator(self, reverse=False):
|
def segment_iterator(self, reverse=False):
|
||||||
data_path = os.path.join(self.path, 'data')
|
data_path = os.path.join(self.path, 'data')
|
||||||
dirs = sorted((dir for dir in os.listdir(data_path) if dir.isdigit()), key=int, reverse=reverse)
|
dirs = sorted((dir for dir in os.listdir(data_path) if dir.isdigit()), key=int, reverse=reverse)
|
||||||
|
@ -535,10 +541,10 @@ class LoggedIO:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_segments_transaction_id(self):
|
def get_segments_transaction_id(self):
|
||||||
"""Verify that the transaction id is consistent with the index transaction id
|
"""Return the last committed segment.
|
||||||
"""
|
"""
|
||||||
for segment, filename in self.segment_iterator(reverse=True):
|
for segment, filename in self.segment_iterator(reverse=True):
|
||||||
if self.is_committed_segment(filename):
|
if self.is_committed_segment(segment):
|
||||||
return segment
|
return segment
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -552,10 +558,14 @@ class LoggedIO:
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
def is_committed_segment(self, filename):
|
def is_committed_segment(self, segment):
|
||||||
"""Check if segment ends with a COMMIT_TAG tag
|
"""Check if segment ends with a COMMIT_TAG tag
|
||||||
"""
|
"""
|
||||||
with open(filename, 'rb') as fd:
|
try:
|
||||||
|
iterator = self.iter_objects(segment)
|
||||||
|
except IntegrityError:
|
||||||
|
return False
|
||||||
|
with open(self.segment_filename(segment), 'rb') as fd:
|
||||||
try:
|
try:
|
||||||
fd.seek(-self.header_fmt.size, os.SEEK_END)
|
fd.seek(-self.header_fmt.size, os.SEEK_END)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@ -563,7 +573,22 @@ class LoggedIO:
|
||||||
if e.errno == errno.EINVAL:
|
if e.errno == errno.EINVAL:
|
||||||
return False
|
return False
|
||||||
raise e
|
raise e
|
||||||
return fd.read(self.header_fmt.size) == self.COMMIT
|
if fd.read(self.header_fmt.size) != self.COMMIT:
|
||||||
|
return False
|
||||||
|
seen_commit = False
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
tag, key, offset = next(iterator)
|
||||||
|
except IntegrityError:
|
||||||
|
return False
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
if tag == TAG_COMMIT:
|
||||||
|
seen_commit = True
|
||||||
|
continue
|
||||||
|
if seen_commit:
|
||||||
|
return False
|
||||||
|
return seen_commit
|
||||||
|
|
||||||
def segment_filename(self, segment):
|
def segment_filename(self, segment):
|
||||||
return os.path.join(self.path, 'data', str(segment // self.segments_per_dir), str(segment))
|
return os.path.join(self.path, 'data', str(segment // self.segments_per_dir), str(segment))
|
||||||
|
@ -578,7 +603,8 @@ class LoggedIO:
|
||||||
dirname = os.path.join(self.path, 'data', str(self.segment // self.segments_per_dir))
|
dirname = os.path.join(self.path, 'data', str(self.segment // self.segments_per_dir))
|
||||||
if not os.path.exists(dirname):
|
if not os.path.exists(dirname):
|
||||||
os.mkdir(dirname)
|
os.mkdir(dirname)
|
||||||
self._write_fd = open(self.segment_filename(self.segment), 'ab')
|
sync_dir(os.path.join(self.path, 'data'))
|
||||||
|
self._write_fd = SyncFile(self.segment_filename(self.segment))
|
||||||
self._write_fd.write(MAGIC)
|
self._write_fd.write(MAGIC)
|
||||||
self.offset = MAGIC_LEN
|
self.offset = MAGIC_LEN
|
||||||
return self._write_fd
|
return self._write_fd
|
||||||
|
@ -591,6 +617,13 @@ class LoggedIO:
|
||||||
self.fds[segment] = fd
|
self.fds[segment] = fd
|
||||||
return fd
|
return fd
|
||||||
|
|
||||||
|
def close_segment(self):
|
||||||
|
if self._write_fd:
|
||||||
|
self.segment += 1
|
||||||
|
self.offset = 0
|
||||||
|
self._write_fd.close()
|
||||||
|
self._write_fd = None
|
||||||
|
|
||||||
def delete_segment(self, segment):
|
def delete_segment(self, segment):
|
||||||
if segment in self.fds:
|
if segment in self.fds:
|
||||||
del self.fds[segment]
|
del self.fds[segment]
|
||||||
|
@ -641,7 +674,7 @@ class LoggedIO:
|
||||||
|
|
||||||
def read(self, segment, offset, id):
|
def read(self, segment, offset, id):
|
||||||
if segment == self.segment and self._write_fd:
|
if segment == self.segment and self._write_fd:
|
||||||
self._write_fd.flush()
|
self._write_fd.sync()
|
||||||
fd = self.get_fd(segment)
|
fd = self.get_fd(segment)
|
||||||
fd.seek(offset)
|
fd.seek(offset)
|
||||||
header = fd.read(self.put_header_fmt.size)
|
header = fd.read(self.put_header_fmt.size)
|
||||||
|
@ -703,20 +736,8 @@ class LoggedIO:
|
||||||
|
|
||||||
def write_commit(self):
|
def write_commit(self):
|
||||||
fd = self.get_write_fd(no_new=True)
|
fd = self.get_write_fd(no_new=True)
|
||||||
|
fd.sync()
|
||||||
header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT)
|
header = self.header_no_crc_fmt.pack(self.header_fmt.size, TAG_COMMIT)
|
||||||
crc = self.crc_fmt.pack(crc32(header) & 0xffffffff)
|
crc = self.crc_fmt.pack(crc32(header) & 0xffffffff)
|
||||||
fd.write(b''.join((crc, header)))
|
fd.write(b''.join((crc, header)))
|
||||||
self.close_segment()
|
self.close_segment()
|
||||||
|
|
||||||
def close_segment(self):
|
|
||||||
if self._write_fd:
|
|
||||||
self.segment += 1
|
|
||||||
self.offset = 0
|
|
||||||
self._write_fd.flush()
|
|
||||||
os.fsync(self._write_fd.fileno())
|
|
||||||
if hasattr(os, 'posix_fadvise'): # only on UNIX
|
|
||||||
# tell the OS that it does not need to cache what we just wrote,
|
|
||||||
# avoids spoiling the cache for the OS and other processes.
|
|
||||||
os.posix_fadvise(self._write_fd.fileno(), 0, 0, os.POSIX_FADV_DONTNEED)
|
|
||||||
self._write_fd.close()
|
|
||||||
self._write_fd = None
|
|
||||||
|
|
|
@ -5,9 +5,13 @@ import posix
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from ..xattr import get_all
|
from ..xattr import get_all
|
||||||
|
from ..platform import get_flags
|
||||||
|
from .. import platform
|
||||||
|
|
||||||
# Note: this is used by borg.selftest, do not use or import py.test functionality here.
|
# Note: this is used by borg.selftest, do not use or import py.test functionality here.
|
||||||
|
|
||||||
|
@ -23,8 +27,20 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raises = None
|
raises = None
|
||||||
|
|
||||||
has_lchflags = hasattr(os, 'lchflags')
|
has_lchflags = hasattr(os, 'lchflags') or sys.platform.startswith('linux')
|
||||||
|
no_lchlfags_because = '' if has_lchflags else '(not supported on this platform)'
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile() as file:
|
||||||
|
platform.set_flags(file.name, stat.UF_NODUMP)
|
||||||
|
except OSError:
|
||||||
|
has_lchflags = False
|
||||||
|
no_lchlfags_because = '(the file system at %s does not support flags)' % tempfile.gettempdir()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import llfuse
|
||||||
|
has_llfuse = True or llfuse # avoids "unused import"
|
||||||
|
except ImportError:
|
||||||
|
has_llfuse = False
|
||||||
|
|
||||||
# The mtime get/set precision varies on different OS and Python versions
|
# The mtime get/set precision varies on different OS and Python versions
|
||||||
if 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []):
|
if 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []):
|
||||||
|
@ -75,13 +91,13 @@ class BaseTestCase(unittest.TestCase):
|
||||||
# Assume path2 is on FUSE if st_dev is different
|
# Assume path2 is on FUSE if st_dev is different
|
||||||
fuse = s1.st_dev != s2.st_dev
|
fuse = s1.st_dev != s2.st_dev
|
||||||
attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
|
attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
|
||||||
if has_lchflags:
|
|
||||||
attrs.append('st_flags')
|
|
||||||
if not fuse or not os.path.isdir(path1):
|
if not fuse or not os.path.isdir(path1):
|
||||||
# dir nlink is always 1 on our fuse filesystem
|
# dir nlink is always 1 on our fuse filesystem
|
||||||
attrs.append('st_nlink')
|
attrs.append('st_nlink')
|
||||||
d1 = [filename] + [getattr(s1, a) for a in attrs]
|
d1 = [filename] + [getattr(s1, a) for a in attrs]
|
||||||
d2 = [filename] + [getattr(s2, a) for a in attrs]
|
d2 = [filename] + [getattr(s2, a) for a in attrs]
|
||||||
|
d1.append(get_flags(path1, s1))
|
||||||
|
d2.append(get_flags(path2, s2))
|
||||||
# ignore st_rdev if file is not a block/char device, fixes #203
|
# ignore st_rdev if file is not a block/char device, fixes #203
|
||||||
if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]):
|
if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]):
|
||||||
d1[4] = None
|
d1[4] = None
|
||||||
|
|
|
@ -1,14 +1,64 @@
|
||||||
|
import os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from io import StringIO
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
import msgpack
|
import msgpack
|
||||||
|
|
||||||
from ..archive import Archive, CacheChunkBuffer, RobustUnpacker
|
from ..archive import Archive, CacheChunkBuffer, RobustUnpacker, Statistics
|
||||||
from ..key import PlaintextKey
|
from ..key import PlaintextKey
|
||||||
from ..helpers import Manifest
|
from ..helpers import Manifest
|
||||||
from . import BaseTestCase
|
from . import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def stats():
|
||||||
|
stats = Statistics()
|
||||||
|
stats.update(20, 10, unique=True)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def test_stats_basic(stats):
|
||||||
|
assert stats.osize == 20
|
||||||
|
assert stats.csize == stats.usize == 10
|
||||||
|
stats.update(20, 10, unique=False)
|
||||||
|
assert stats.osize == 40
|
||||||
|
assert stats.csize == 20
|
||||||
|
assert stats.usize == 10
|
||||||
|
|
||||||
|
|
||||||
|
def tests_stats_progress(stats, columns=80):
|
||||||
|
os.environ['COLUMNS'] = str(columns)
|
||||||
|
out = StringIO()
|
||||||
|
stats.show_progress(stream=out)
|
||||||
|
s = '20 B O 10 B C 10 B D 0 N '
|
||||||
|
buf = ' ' * (columns - len(s))
|
||||||
|
assert out.getvalue() == s + buf + "\r"
|
||||||
|
|
||||||
|
out = StringIO()
|
||||||
|
stats.update(10**3, 0, unique=False)
|
||||||
|
stats.show_progress(item={b'path': 'foo'}, final=False, stream=out)
|
||||||
|
s = '1.02 kB O 10 B C 10 B D 0 N foo'
|
||||||
|
buf = ' ' * (columns - len(s))
|
||||||
|
assert out.getvalue() == s + buf + "\r"
|
||||||
|
out = StringIO()
|
||||||
|
stats.show_progress(item={b'path': 'foo'*40}, final=False, stream=out)
|
||||||
|
s = '1.02 kB O 10 B C 10 B D 0 N foofoofoofoofoofoofoofo...oofoofoofoofoofoofoofoofoo'
|
||||||
|
buf = ' ' * (columns - len(s))
|
||||||
|
assert out.getvalue() == s + buf + "\r"
|
||||||
|
|
||||||
|
|
||||||
|
def test_stats_format(stats):
|
||||||
|
assert str(stats) == """\
|
||||||
|
Original size Compressed size Deduplicated size
|
||||||
|
This archive: 20 B 10 B 10 B"""
|
||||||
|
s = "{0.osize_fmt}".format(stats)
|
||||||
|
assert s == "20 B"
|
||||||
|
# kind of redundant, but id is variable so we can't match reliably
|
||||||
|
assert repr(stats) == '<Statistics object at {:#x} (20, 10, 10)>'.format(id(stats))
|
||||||
|
|
||||||
|
|
||||||
class MockCache:
|
class MockCache:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
|
@ -3,6 +3,7 @@ import errno
|
||||||
import os
|
import os
|
||||||
import inspect
|
import inspect
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -16,7 +17,7 @@ from hashlib import sha256
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .. import xattr, helpers
|
from .. import xattr, helpers, platform
|
||||||
from ..archive import Archive, ChunkBuffer, ArchiveRecreater
|
from ..archive import Archive, ChunkBuffer, ArchiveRecreater
|
||||||
from ..archiver import Archiver
|
from ..archiver import Archiver
|
||||||
from ..cache import Cache
|
from ..cache import Cache
|
||||||
|
@ -26,15 +27,13 @@ from ..helpers import Chunk, Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, b
|
||||||
from ..key import KeyfileKeyBase
|
from ..key import KeyfileKeyBase
|
||||||
from ..remote import RemoteRepository, PathNotAllowed
|
from ..remote import RemoteRepository, PathNotAllowed
|
||||||
from ..repository import Repository
|
from ..repository import Repository
|
||||||
|
from . import has_lchflags, has_llfuse
|
||||||
from . import BaseTestCase, changedir, environment_variable
|
from . import BaseTestCase, changedir, environment_variable
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import llfuse
|
import llfuse
|
||||||
has_llfuse = True or llfuse # avoids "unused import"
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
has_llfuse = False
|
pass
|
||||||
|
|
||||||
has_lchflags = hasattr(os, 'lchflags')
|
|
||||||
|
|
||||||
src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
@ -280,7 +279,7 @@ class ArchiverTestCaseBase(BaseTestCase):
|
||||||
# FIFO node
|
# FIFO node
|
||||||
os.mkfifo(os.path.join(self.input_path, 'fifo1'))
|
os.mkfifo(os.path.join(self.input_path, 'fifo1'))
|
||||||
if has_lchflags:
|
if has_lchflags:
|
||||||
os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
|
platform.set_flags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
|
||||||
try:
|
try:
|
||||||
# Block device
|
# Block device
|
||||||
os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
|
os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
|
||||||
|
@ -299,9 +298,14 @@ class ArchiverTestCaseBase(BaseTestCase):
|
||||||
class ArchiverTestCase(ArchiverTestCaseBase):
|
class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
def test_basic_functionality(self):
|
def test_basic_functionality(self):
|
||||||
have_root = self.create_test_files()
|
have_root = self.create_test_files()
|
||||||
self.cmd('init', self.repository_location)
|
# fork required to test show-rc output
|
||||||
|
output = self.cmd('init', '--show-version', '--show-rc', self.repository_location, fork=True)
|
||||||
|
self.assert_in('borgbackup version', output)
|
||||||
|
self.assert_in('terminating with success status, rc 0', output)
|
||||||
self.cmd('create', self.repository_location + '::test', 'input')
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
|
output = self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
|
||||||
|
self.assert_in('Archive name: test.2', output)
|
||||||
|
self.assert_in('This archive: ', output)
|
||||||
with changedir('output'):
|
with changedir('output'):
|
||||||
self.cmd('extract', self.repository_location + '::test')
|
self.cmd('extract', self.repository_location + '::test')
|
||||||
list_output = self.cmd('list', '--short', self.repository_location)
|
list_output = self.cmd('list', '--short', self.repository_location)
|
||||||
|
@ -353,6 +357,14 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
# the interesting parts of info_output2 and info_output should be same
|
# the interesting parts of info_output2 and info_output should be same
|
||||||
self.assert_equal(filter(info_output), filter(info_output2))
|
self.assert_equal(filter(info_output), filter(info_output2))
|
||||||
|
|
||||||
|
def test_symlink_extract(self):
|
||||||
|
self.create_test_files()
|
||||||
|
self.cmd('init', self.repository_location)
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
with changedir('output'):
|
||||||
|
self.cmd('extract', self.repository_location + '::test')
|
||||||
|
assert os.readlink('input/link1') == 'somewhere'
|
||||||
|
|
||||||
def test_atime(self):
|
def test_atime(self):
|
||||||
self.create_test_files()
|
self.create_test_files()
|
||||||
atime, mtime = 123456780, 234567890
|
atime, mtime = 123456780, 234567890
|
||||||
|
@ -639,6 +651,31 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
|
self.cmd("extract", self.repository_location + "::test", "fm:input/file1", "fm:*file33*", "input/file2")
|
||||||
self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
|
self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file333"])
|
||||||
|
|
||||||
|
def test_extract_list_output(self):
|
||||||
|
self.cmd('init', self.repository_location)
|
||||||
|
self.create_regular_file('file', size=1024 * 80)
|
||||||
|
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
|
||||||
|
with changedir('output'):
|
||||||
|
output = self.cmd('extract', self.repository_location + '::test')
|
||||||
|
self.assert_not_in("input/file", output)
|
||||||
|
shutil.rmtree('output/input')
|
||||||
|
|
||||||
|
with changedir('output'):
|
||||||
|
output = self.cmd('extract', '--info', self.repository_location + '::test')
|
||||||
|
self.assert_not_in("input/file", output)
|
||||||
|
shutil.rmtree('output/input')
|
||||||
|
|
||||||
|
with changedir('output'):
|
||||||
|
output = self.cmd('extract', '--list', self.repository_location + '::test')
|
||||||
|
self.assert_in("input/file", output)
|
||||||
|
shutil.rmtree('output/input')
|
||||||
|
|
||||||
|
with changedir('output'):
|
||||||
|
output = self.cmd('extract', '--list', '--info', self.repository_location + '::test')
|
||||||
|
self.assert_in("input/file", output)
|
||||||
|
|
||||||
def _create_test_caches(self):
|
def _create_test_caches(self):
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
self.create_regular_file('file1', size=1024 * 80)
|
self.create_regular_file('file1', size=1024 * 80)
|
||||||
|
@ -851,7 +888,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
|
self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
|
||||||
self.cmd('delete', self.repository_location + '::test')
|
self.cmd('delete', self.repository_location + '::test')
|
||||||
self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
|
self.cmd('extract', '--dry-run', self.repository_location + '::test.2')
|
||||||
self.cmd('delete', '--stats', self.repository_location + '::test.2')
|
output = self.cmd('delete', '--stats', self.repository_location + '::test.2')
|
||||||
|
self.assert_in('Deleted data:', output)
|
||||||
# Make sure all data except the manifest has been deleted
|
# Make sure all data except the manifest has been deleted
|
||||||
with Repository(self.repository_path) as repository:
|
with Repository(self.repository_path) as repository:
|
||||||
self.assert_equal(len(repository), 1)
|
self.assert_equal(len(repository), 1)
|
||||||
|
@ -874,12 +912,16 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
self.create_src_archive('test')
|
self.create_src_archive('test')
|
||||||
self.cmd('extract', '--dry-run', self.repository_location + '::test')
|
self.cmd('extract', '--dry-run', self.repository_location + '::test')
|
||||||
self.cmd('check', self.repository_location)
|
output = self.cmd('check', '--show-version', self.repository_location)
|
||||||
|
self.assert_in('borgbackup version', output) # implied output even without --info given
|
||||||
|
self.assert_not_in('Starting repository check', output) # --info not given for root logger
|
||||||
|
|
||||||
name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
|
name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
|
||||||
with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
|
with open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+b') as fd:
|
||||||
fd.seek(100)
|
fd.seek(100)
|
||||||
fd.write(b'XXXX')
|
fd.write(b'XXXX')
|
||||||
self.cmd('check', self.repository_location, exit_code=1)
|
output = self.cmd('check', '--info', self.repository_location, exit_code=1)
|
||||||
|
self.assert_in('Starting repository check', output) # --info given for root logger
|
||||||
|
|
||||||
# we currently need to be able to create a lock directory inside the repo:
|
# we currently need to be able to create a lock directory inside the repo:
|
||||||
@pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
|
@pytest.mark.xfail(reason="we need to be able to create the lock directory inside the repo")
|
||||||
|
@ -927,11 +969,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
|
os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
|
||||||
self.create_regular_file('file2', size=1024 * 80)
|
self.create_regular_file('file2', size=1024 * 80)
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
|
output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
|
||||||
self.assert_in("A input/file1", output)
|
self.assert_in("A input/file1", output)
|
||||||
self.assert_in("A input/file2", output)
|
self.assert_in("A input/file2", output)
|
||||||
# should find first file as unmodified
|
# should find first file as unmodified
|
||||||
output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input')
|
output = self.cmd('create', '--list', self.repository_location + '::test1', 'input')
|
||||||
self.assert_in("U input/file1", output)
|
self.assert_in("U input/file1", output)
|
||||||
# this is expected, although surprising, for why, see:
|
# this is expected, although surprising, for why, see:
|
||||||
# https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
|
# https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
|
||||||
|
@ -945,11 +987,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
|
os.utime('input/file1', (now - 5, now - 5)) # 5 seconds ago
|
||||||
self.create_regular_file('file2', size=1024 * 80)
|
self.create_regular_file('file2', size=1024 * 80)
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
output = self.cmd('create', '-v', '--list', self.repository_location + '::test', 'input')
|
output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
|
||||||
self.assert_in("A input/file1", output)
|
self.assert_in("A input/file1", output)
|
||||||
self.assert_in("A input/file2", output)
|
self.assert_in("A input/file2", output)
|
||||||
# should find second file as excluded
|
# should find second file as excluded
|
||||||
output = self.cmd('create', '-v', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
|
output = self.cmd('create', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
|
||||||
self.assert_in("U input/file1", output)
|
self.assert_in("U input/file1", output)
|
||||||
self.assert_in("x input/file2", output)
|
self.assert_in("x input/file2", output)
|
||||||
|
|
||||||
|
@ -966,15 +1008,15 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
output = self.cmd('create', self.repository_location + '::test0', 'input')
|
output = self.cmd('create', self.repository_location + '::test0', 'input')
|
||||||
self.assert_not_in('file1', output)
|
self.assert_not_in('file1', output)
|
||||||
# should list the file as unchanged
|
# should list the file as unchanged
|
||||||
output = self.cmd('create', '-v', '--list', '--filter=U', self.repository_location + '::test1', 'input')
|
output = self.cmd('create', '--list', '--filter=U', self.repository_location + '::test1', 'input')
|
||||||
self.assert_in('file1', output)
|
self.assert_in('file1', output)
|
||||||
# should *not* list the file as changed
|
# should *not* list the file as changed
|
||||||
output = self.cmd('create', '-v', '--filter=AM', self.repository_location + '::test2', 'input')
|
output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test2', 'input')
|
||||||
self.assert_not_in('file1', output)
|
self.assert_not_in('file1', output)
|
||||||
# change the file
|
# change the file
|
||||||
self.create_regular_file('file1', size=1024 * 100)
|
self.create_regular_file('file1', size=1024 * 100)
|
||||||
# should list the file as changed
|
# should list the file as changed
|
||||||
output = self.cmd('create', '-v', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
|
output = self.cmd('create', '--list', '--filter=AM', self.repository_location + '::test3', 'input')
|
||||||
self.assert_in('file1', output)
|
self.assert_in('file1', output)
|
||||||
|
|
||||||
# def test_cmdline_compatibility(self):
|
# def test_cmdline_compatibility(self):
|
||||||
|
@ -992,7 +1034,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
|
self.cmd('create', self.repository_location + '::test3.checkpoint', src_dir)
|
||||||
self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
|
self.cmd('create', self.repository_location + '::test3.checkpoint.1', src_dir)
|
||||||
self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
|
self.cmd('create', self.repository_location + '::test4.checkpoint', src_dir)
|
||||||
output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
|
output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
|
||||||
|
self.assert_in('Keeping archive: test2', output)
|
||||||
self.assert_in('Would prune: test1', output)
|
self.assert_in('Would prune: test1', output)
|
||||||
# must keep the latest non-checkpoint archive:
|
# must keep the latest non-checkpoint archive:
|
||||||
self.assert_in('Keeping archive: test2', output)
|
self.assert_in('Keeping archive: test2', output)
|
||||||
|
@ -1026,9 +1069,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
self.cmd('create', self.repository_location + '::test1', src_dir)
|
self.cmd('create', self.repository_location + '::test1', src_dir)
|
||||||
self.cmd('create', self.repository_location + '::test2', src_dir)
|
self.cmd('create', self.repository_location + '::test2', src_dir)
|
||||||
output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2')
|
output = self.cmd('prune', '--list', '--stats', '--dry-run', self.repository_location, '--keep-daily=2')
|
||||||
self.assert_in('Keeping archive: test2', output)
|
self.assert_in('Keeping archive: test2', output)
|
||||||
self.assert_in('Would prune: test1', output)
|
self.assert_in('Would prune: test1', output)
|
||||||
|
self.assert_in('Deleted data:', output)
|
||||||
output = self.cmd('list', self.repository_location)
|
output = self.cmd('list', self.repository_location)
|
||||||
self.assert_in('test1', output)
|
self.assert_in('test1', output)
|
||||||
self.assert_in('test2', output)
|
self.assert_in('test2', output)
|
||||||
|
@ -1043,7 +1087,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
|
self.cmd('create', self.repository_location + '::foo-2015-08-12-20:00', src_dir)
|
||||||
self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
|
self.cmd('create', self.repository_location + '::bar-2015-08-12-10:00', src_dir)
|
||||||
self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
|
self.cmd('create', self.repository_location + '::bar-2015-08-12-20:00', src_dir)
|
||||||
output = self.cmd('prune', '-v', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
|
output = self.cmd('prune', '--list', '--dry-run', self.repository_location, '--keep-daily=2', '--prefix=foo-')
|
||||||
self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
|
self.assert_in('Keeping archive: foo-2015-08-12-20:00', output)
|
||||||
self.assert_in('Would prune: foo-2015-08-12-10:00', output)
|
self.assert_in('Would prune: foo-2015-08-12-10:00', output)
|
||||||
output = self.cmd('list', self.repository_location)
|
output = self.cmd('list', self.repository_location)
|
||||||
|
@ -1395,7 +1439,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
if interrupt_early:
|
if interrupt_early:
|
||||||
process_files = 0
|
process_files = 0
|
||||||
with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
|
with patch.object(ArchiveRecreater, 'process_item', self._recreate_interrupt_patch(process_files)):
|
||||||
self.cmd('recreate', '-sv', '--list', self.repository_location, 'input/dir2')
|
self.cmd('recreate', self.repository_location, 'input/dir2')
|
||||||
assert 'test.recreate' in self.cmd('list', self.repository_location)
|
assert 'test.recreate' in self.cmd('list', self.repository_location)
|
||||||
if change_args:
|
if change_args:
|
||||||
with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
|
with patch.object(sys, 'argv', sys.argv + ['non-forking tests don\'t use sys.argv']):
|
||||||
|
@ -1486,6 +1530,32 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
|
cmd = 'python3', '-c', 'import os, sys; sys.exit(42 if os.path.exists("%s") else 23)' % lock_path
|
||||||
self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
|
self.cmd('with-lock', self.repository_location, *cmd, fork=True, exit_code=42)
|
||||||
|
|
||||||
|
def test_recreate_list_output(self):
|
||||||
|
self.cmd('init', self.repository_location)
|
||||||
|
self.create_regular_file('file1', size=0)
|
||||||
|
self.create_regular_file('file2', size=0)
|
||||||
|
self.create_regular_file('file3', size=0)
|
||||||
|
self.create_regular_file('file4', size=0)
|
||||||
|
self.create_regular_file('file5', size=0)
|
||||||
|
|
||||||
|
self.cmd('create', self.repository_location + '::test', 'input')
|
||||||
|
|
||||||
|
output = self.cmd('recreate', '--list', '--info', self.repository_location + '::test', '-e', 'input/file2')
|
||||||
|
self.assert_in("input/file1", output)
|
||||||
|
self.assert_in("x input/file2", output)
|
||||||
|
|
||||||
|
output = self.cmd('recreate', '--list', self.repository_location + '::test', '-e', 'input/file3')
|
||||||
|
self.assert_in("input/file1", output)
|
||||||
|
self.assert_in("x input/file3", output)
|
||||||
|
|
||||||
|
output = self.cmd('recreate', self.repository_location + '::test', '-e', 'input/file4')
|
||||||
|
self.assert_not_in("input/file1", output)
|
||||||
|
self.assert_not_in("x input/file4", output)
|
||||||
|
|
||||||
|
output = self.cmd('recreate', '--info', self.repository_location + '::test', '-e', 'input/file5')
|
||||||
|
self.assert_not_in("input/file1", output)
|
||||||
|
self.assert_not_in("x input/file5", output)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
|
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
|
||||||
class ArchiverTestCaseBinary(ArchiverTestCase):
|
class ArchiverTestCaseBinary(ArchiverTestCase):
|
||||||
|
@ -1526,12 +1596,16 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
||||||
return archive, repository
|
return archive, repository
|
||||||
|
|
||||||
def test_check_usage(self):
|
def test_check_usage(self):
|
||||||
output = self.cmd('check', '-v', self.repository_location, exit_code=0)
|
output = self.cmd('check', '-v', '--progress', self.repository_location, exit_code=0)
|
||||||
self.assert_in('Starting repository check', output)
|
self.assert_in('Starting repository check', output)
|
||||||
self.assert_in('Starting archive consistency check', output)
|
self.assert_in('Starting archive consistency check', output)
|
||||||
|
self.assert_in('Checking segments', output)
|
||||||
|
# reset logging to new process default to avoid need for fork=True on next check
|
||||||
|
logging.getLogger('borg.output.progress').setLevel(logging.NOTSET)
|
||||||
output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
|
output = self.cmd('check', '-v', '--repository-only', self.repository_location, exit_code=0)
|
||||||
self.assert_in('Starting repository check', output)
|
self.assert_in('Starting repository check', output)
|
||||||
self.assert_not_in('Starting archive consistency check', output)
|
self.assert_not_in('Starting archive consistency check', output)
|
||||||
|
self.assert_not_in('Checking segments', output)
|
||||||
output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
|
output = self.cmd('check', '-v', '--archives-only', self.repository_location, exit_code=0)
|
||||||
self.assert_not_in('Starting repository check', output)
|
self.assert_not_in('Starting repository check', output)
|
||||||
self.assert_in('Starting archive consistency check', output)
|
self.assert_in('Starting archive consistency check', output)
|
||||||
|
@ -1590,6 +1664,29 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
||||||
self.cmd('check', self.repository_location, exit_code=0)
|
self.cmd('check', self.repository_location, exit_code=0)
|
||||||
self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
|
self.cmd('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
|
||||||
|
|
||||||
|
def _test_verify_data(self, *init_args):
|
||||||
|
shutil.rmtree(self.repository_path)
|
||||||
|
self.cmd('init', self.repository_location, *init_args)
|
||||||
|
self.create_src_archive('archive1')
|
||||||
|
archive, repository = self.open_archive('archive1')
|
||||||
|
with repository:
|
||||||
|
for item in archive.iter_items():
|
||||||
|
if item[b'path'].endswith('testsuite/archiver.py'):
|
||||||
|
chunk = item[b'chunks'][-1]
|
||||||
|
data = repository.get(chunk.id) + b'1234'
|
||||||
|
repository.put(chunk.id, data)
|
||||||
|
break
|
||||||
|
repository.commit()
|
||||||
|
self.cmd('check', self.repository_location, exit_code=0)
|
||||||
|
output = self.cmd('check', '--verify-data', self.repository_location, exit_code=1)
|
||||||
|
assert bin_to_hex(chunk.id) + ', integrity error' in output
|
||||||
|
|
||||||
|
def test_verify_data(self):
|
||||||
|
self._test_verify_data('--encryption', 'repokey')
|
||||||
|
|
||||||
|
def test_verify_data_unencrypted(self):
|
||||||
|
self._test_verify_data('--encryption', 'none')
|
||||||
|
|
||||||
|
|
||||||
class RemoteArchiverTestCase(ArchiverTestCase):
|
class RemoteArchiverTestCase(ArchiverTestCase):
|
||||||
prefix = '__testsuite__:'
|
prefix = '__testsuite__:'
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
from ..logger import setup_logging
|
|
||||||
|
|
||||||
# Ensure that the loggers exist for all tests
|
|
||||||
setup_logging()
|
|
|
@ -1,7 +1,8 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
from time import mktime, strptime
|
from time import mktime, strptime
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from io import StringIO
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -11,7 +12,7 @@ import msgpack.fallback
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..helpers import Location, format_file_size, format_timedelta, make_path_safe, clean_lines, \
|
from ..helpers import Location, format_file_size, format_timedelta, make_path_safe, clean_lines, \
|
||||||
prune_within, prune_split, get_cache_dir, get_keys_dir, Statistics, is_slow_msgpack, \
|
prune_within, prune_split, get_cache_dir, get_keys_dir, is_slow_msgpack, \
|
||||||
yes, TRUISH, FALSISH, DEFAULTISH, \
|
yes, TRUISH, FALSISH, DEFAULTISH, \
|
||||||
StableDict, int_to_bigint, bigint_to_int, bin_to_hex, parse_timestamp, ChunkerParams, Chunk, \
|
StableDict, int_to_bigint, bigint_to_int, bin_to_hex, parse_timestamp, ChunkerParams, Chunk, \
|
||||||
ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
|
ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
|
||||||
|
@ -629,53 +630,6 @@ def test_get_keys_dir():
|
||||||
os.environ['BORG_KEYS_DIR'] = old_env
|
os.environ['BORG_KEYS_DIR'] = old_env
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def stats():
|
|
||||||
stats = Statistics()
|
|
||||||
stats.update(20, 10, unique=True)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
def test_stats_basic(stats):
|
|
||||||
assert stats.osize == 20
|
|
||||||
assert stats.csize == stats.usize == 10
|
|
||||||
stats.update(20, 10, unique=False)
|
|
||||||
assert stats.osize == 40
|
|
||||||
assert stats.csize == 20
|
|
||||||
assert stats.usize == 10
|
|
||||||
|
|
||||||
|
|
||||||
def tests_stats_progress(stats, columns=80):
|
|
||||||
os.environ['COLUMNS'] = str(columns)
|
|
||||||
out = StringIO()
|
|
||||||
stats.show_progress(stream=out)
|
|
||||||
s = '20 B O 10 B C 10 B D 0 N '
|
|
||||||
buf = ' ' * (columns - len(s))
|
|
||||||
assert out.getvalue() == s + buf + "\r"
|
|
||||||
|
|
||||||
out = StringIO()
|
|
||||||
stats.update(10**3, 0, unique=False)
|
|
||||||
stats.show_progress(item={b'path': 'foo'}, final=False, stream=out)
|
|
||||||
s = '1.02 kB O 10 B C 10 B D 0 N foo'
|
|
||||||
buf = ' ' * (columns - len(s))
|
|
||||||
assert out.getvalue() == s + buf + "\r"
|
|
||||||
out = StringIO()
|
|
||||||
stats.show_progress(item={b'path': 'foo'*40}, final=False, stream=out)
|
|
||||||
s = '1.02 kB O 10 B C 10 B D 0 N foofoofoofoofoofoofoofo...oofoofoofoofoofoofoofoofoo'
|
|
||||||
buf = ' ' * (columns - len(s))
|
|
||||||
assert out.getvalue() == s + buf + "\r"
|
|
||||||
|
|
||||||
|
|
||||||
def test_stats_format(stats):
|
|
||||||
assert str(stats) == """\
|
|
||||||
Original size Compressed size Deduplicated size
|
|
||||||
This archive: 20 B 10 B 10 B"""
|
|
||||||
s = "{0.osize_fmt}".format(stats)
|
|
||||||
assert s == "20 B"
|
|
||||||
# kind of redundant, but id is variable so we can't match reliably
|
|
||||||
assert repr(stats) == '<Statistics object at {:#x} (20, 10, 10)>'.format(id(stats))
|
|
||||||
|
|
||||||
|
|
||||||
def test_file_size():
|
def test_file_size():
|
||||||
"""test the size formatting routines"""
|
"""test the size formatting routines"""
|
||||||
si_size_map = {
|
si_size_map = {
|
||||||
|
@ -826,7 +780,7 @@ def test_yes_output(capfd):
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_multiline(capfd):
|
def test_progress_percentage_multiline(capfd):
|
||||||
pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%", file=sys.stderr)
|
pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
|
||||||
pi.show(0)
|
pi.show(0)
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ' 0%\n'
|
assert err == ' 0%\n'
|
||||||
|
@ -842,13 +796,14 @@ def test_progress_percentage_multiline(capfd):
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_sameline(capfd):
|
def test_progress_percentage_sameline(capfd):
|
||||||
pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%", file=sys.stderr)
|
pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%")
|
||||||
pi.show(0)
|
pi.show(0)
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ' 0%\r'
|
assert err == ' 0%\r'
|
||||||
pi.show(420)
|
pi.show(420)
|
||||||
|
pi.show(680)
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ' 42%\r'
|
assert err == ' 42%\r 68%\r'
|
||||||
pi.show(1000)
|
pi.show(1000)
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == '100%\r'
|
assert err == '100%\r'
|
||||||
|
@ -858,7 +813,7 @@ def test_progress_percentage_sameline(capfd):
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_step(capfd):
|
def test_progress_percentage_step(capfd):
|
||||||
pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%", file=sys.stderr)
|
pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%")
|
||||||
pi.show()
|
pi.show()
|
||||||
out, err = capfd.readouterr()
|
out, err = capfd.readouterr()
|
||||||
assert err == ' 0%\n'
|
assert err == ' 0%\n'
|
||||||
|
@ -870,6 +825,21 @@ def test_progress_percentage_step(capfd):
|
||||||
assert err == ' 2%\n'
|
assert err == ' 2%\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_progress_percentage_quiet(capfd):
|
||||||
|
logging.getLogger('borg.output.progress').setLevel(logging.WARN)
|
||||||
|
|
||||||
|
pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
|
||||||
|
pi.show(0)
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
pi.show(1000)
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
pi.finish()
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert err == ''
|
||||||
|
|
||||||
|
|
||||||
def test_progress_endless(capfd):
|
def test_progress_endless(capfd):
|
||||||
pi = ProgressIndicatorEndless(step=1, file=sys.stderr)
|
pi = ProgressIndicatorEndless(step=1, file=sys.stderr)
|
||||||
pi.show()
|
pi.show()
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ..item import Item
|
||||||
|
from ..helpers import StableDict
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_empty():
|
||||||
|
item = Item()
|
||||||
|
|
||||||
|
assert item.as_dict() == {}
|
||||||
|
|
||||||
|
assert 'path' not in item
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
'invalid-key' in item
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
b'path' in item
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
42 in item
|
||||||
|
|
||||||
|
assert item.get('mode') is None
|
||||||
|
assert item.get('mode', 0o666) == 0o666
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
item.get('invalid-key')
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
item.get(b'mode')
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
item.get(42)
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
item.path
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
del item.path
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_from_dict():
|
||||||
|
# does not matter whether we get str or bytes keys
|
||||||
|
item = Item({b'path': b'/a/b/c', b'mode': 0o666})
|
||||||
|
assert item.path == '/a/b/c'
|
||||||
|
assert item.mode == 0o666
|
||||||
|
assert 'path' in item
|
||||||
|
|
||||||
|
# does not matter whether we get str or bytes keys
|
||||||
|
item = Item({'path': b'/a/b/c', 'mode': 0o666})
|
||||||
|
assert item.path == '/a/b/c'
|
||||||
|
assert item.mode == 0o666
|
||||||
|
assert 'mode' in item
|
||||||
|
|
||||||
|
# invalid - no dict
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Item(42)
|
||||||
|
|
||||||
|
# invalid - no bytes/str key
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Item({42: 23})
|
||||||
|
|
||||||
|
# invalid - unknown key
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Item({'foobar': 'baz'})
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_from_kw():
|
||||||
|
item = Item(path=b'/a/b/c', mode=0o666)
|
||||||
|
assert item.path == '/a/b/c'
|
||||||
|
assert item.mode == 0o666
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_int_property():
|
||||||
|
item = Item()
|
||||||
|
item.mode = 0o666
|
||||||
|
assert item.mode == 0o666
|
||||||
|
assert item.as_dict() == {'mode': 0o666}
|
||||||
|
del item.mode
|
||||||
|
assert item.as_dict() == {}
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
item.mode = "invalid"
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_bigint_property():
|
||||||
|
item = Item()
|
||||||
|
small, big = 42, 2 ** 65
|
||||||
|
item.atime = small
|
||||||
|
assert item.atime == small
|
||||||
|
assert item.as_dict() == {'atime': small}
|
||||||
|
item.atime = big
|
||||||
|
assert item.atime == big
|
||||||
|
assert item.as_dict() == {'atime': b'\0' * 8 + b'\x02'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_user_group_none():
|
||||||
|
item = Item()
|
||||||
|
item.user = None
|
||||||
|
assert item.user is None
|
||||||
|
item.group = None
|
||||||
|
assert item.group is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_se_str_property():
|
||||||
|
# start simple
|
||||||
|
item = Item()
|
||||||
|
item.path = '/a/b/c'
|
||||||
|
assert item.path == '/a/b/c'
|
||||||
|
assert item.as_dict() == {'path': b'/a/b/c'}
|
||||||
|
del item.path
|
||||||
|
assert item.as_dict() == {}
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
item.path = 42
|
||||||
|
|
||||||
|
# non-utf-8 path, needing surrogate-escaping for latin-1 u-umlaut
|
||||||
|
item = Item({'path': b'/a/\xfc/c'})
|
||||||
|
assert item.path == '/a/\udcfc/c' # getting a surrogate-escaped representation
|
||||||
|
assert item.as_dict() == {'path': b'/a/\xfc/c'}
|
||||||
|
del item.path
|
||||||
|
assert 'path' not in item
|
||||||
|
item.path = '/a/\udcfc/c' # setting using a surrogate-escaped representation
|
||||||
|
assert item.as_dict() == {'path': b'/a/\xfc/c'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_list_property():
|
||||||
|
item = Item()
|
||||||
|
item.chunks = []
|
||||||
|
assert item.chunks == []
|
||||||
|
item.chunks.append(0)
|
||||||
|
assert item.chunks == [0]
|
||||||
|
item.chunks.append(1)
|
||||||
|
assert item.chunks == [0, 1]
|
||||||
|
assert item.as_dict() == {'chunks': [0, 1]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_dict_property():
|
||||||
|
item = Item()
|
||||||
|
item.xattrs = StableDict()
|
||||||
|
assert item.xattrs == StableDict()
|
||||||
|
item.xattrs['foo'] = 'bar'
|
||||||
|
assert item.xattrs['foo'] == 'bar'
|
||||||
|
item.xattrs['bar'] = 'baz'
|
||||||
|
assert item.xattrs == StableDict({'foo': 'bar', 'bar': 'baz'})
|
||||||
|
assert item.as_dict() == {'xattrs': {'foo': 'bar', 'bar': 'baz'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_property():
|
||||||
|
# we do not want the user to be able to set unknown attributes -
|
||||||
|
# they won't get into the .as_dict() result dictionary.
|
||||||
|
# also they might be just typos of known attributes.
|
||||||
|
item = Item()
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
item.unknown_attribute = None
|
|
@ -1,17 +1,24 @@
|
||||||
|
import getpass
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from ..crypto import bytes_to_long, num_aes_blocks
|
from ..crypto import bytes_to_long, num_aes_blocks
|
||||||
from ..key import PlaintextKey, PassphraseKey, KeyfileKey
|
from ..key import PlaintextKey, PassphraseKey, KeyfileKey, Passphrase, PasswordRetriesExceeded, bin_to_hex
|
||||||
from ..helpers import Location, Chunk, bin_to_hex
|
from ..helpers import Location, Chunk, IntegrityError
|
||||||
from . import BaseTestCase, environment_variable
|
|
||||||
|
|
||||||
|
|
||||||
class KeyTestCase(BaseTestCase):
|
@pytest.fixture(autouse=True)
|
||||||
|
def clean_env(monkeypatch):
|
||||||
|
# Workaround for some tests (testsuite/archiver) polluting the environment
|
||||||
|
monkeypatch.delenv('BORG_PASSPHRASE', False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestKey:
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
location = Location(tempfile.mkstemp()[1])
|
location = Location(tempfile.mkstemp()[1])
|
||||||
|
|
||||||
|
@ -31,14 +38,10 @@ class KeyTestCase(BaseTestCase):
|
||||||
"""))
|
"""))
|
||||||
keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
|
keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
|
||||||
|
|
||||||
def setUp(self):
|
@pytest.fixture
|
||||||
self.tmppath = tempfile.mkdtemp()
|
def keys_dir(self, request, monkeypatch, tmpdir):
|
||||||
os.environ['BORG_KEYS_DIR'] = self.tmppath
|
monkeypatch.setenv('BORG_KEYS_DIR', tmpdir)
|
||||||
self.tmppath2 = tempfile.mkdtemp()
|
return tmpdir
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(self.tmppath)
|
|
||||||
shutil.rmtree(self.tmppath2)
|
|
||||||
|
|
||||||
class MockRepository:
|
class MockRepository:
|
||||||
class _Location:
|
class _Location:
|
||||||
|
@ -51,78 +54,144 @@ class KeyTestCase(BaseTestCase):
|
||||||
def test_plaintext(self):
|
def test_plaintext(self):
|
||||||
key = PlaintextKey.create(None, None)
|
key = PlaintextKey.create(None, None)
|
||||||
chunk = Chunk(b'foo')
|
chunk = Chunk(b'foo')
|
||||||
self.assert_equal(hexlify(key.id_hash(chunk.data)), b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae')
|
assert hexlify(key.id_hash(chunk.data)) == b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'
|
||||||
self.assert_equal(chunk, key.decrypt(key.id_hash(chunk.data), key.encrypt(chunk)))
|
assert chunk == key.decrypt(key.id_hash(chunk.data), key.encrypt(chunk))
|
||||||
|
|
||||||
def test_keyfile(self):
|
def test_keyfile(self, monkeypatch, keys_dir):
|
||||||
os.environ['BORG_PASSPHRASE'] = 'test'
|
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
|
||||||
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
|
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
|
||||||
self.assert_equal(bytes_to_long(key.enc_cipher.iv, 8), 0)
|
assert bytes_to_long(key.enc_cipher.iv, 8) == 0
|
||||||
manifest = key.encrypt(Chunk(b'XXX'))
|
manifest = key.encrypt(Chunk(b'XXX'))
|
||||||
self.assert_equal(key.extract_nonce(manifest), 0)
|
assert key.extract_nonce(manifest) == 0
|
||||||
manifest2 = key.encrypt(Chunk(b'XXX'))
|
manifest2 = key.encrypt(Chunk(b'XXX'))
|
||||||
self.assert_not_equal(manifest, manifest2)
|
assert manifest != manifest2
|
||||||
self.assert_equal(key.decrypt(None, manifest), key.decrypt(None, manifest2))
|
assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
|
||||||
self.assert_equal(key.extract_nonce(manifest2), 1)
|
assert key.extract_nonce(manifest2) == 1
|
||||||
iv = key.extract_nonce(manifest)
|
iv = key.extract_nonce(manifest)
|
||||||
key2 = KeyfileKey.detect(self.MockRepository(), manifest)
|
key2 = KeyfileKey.detect(self.MockRepository(), manifest)
|
||||||
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD))
|
assert bytes_to_long(key2.enc_cipher.iv, 8) == iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD)
|
||||||
# Key data sanity check
|
# Key data sanity check
|
||||||
self.assert_equal(len(set([key2.id_key, key2.enc_key, key2.enc_hmac_key])), 3)
|
assert len({key2.id_key, key2.enc_key, key2.enc_hmac_key}) == 3
|
||||||
self.assert_equal(key2.chunk_seed == 0, False)
|
assert key2.chunk_seed != 0
|
||||||
chunk = Chunk(b'foo')
|
chunk = Chunk(b'foo')
|
||||||
self.assert_equal(chunk, key2.decrypt(key.id_hash(chunk.data), key.encrypt(chunk)))
|
assert chunk == key2.decrypt(key.id_hash(chunk.data), key.encrypt(chunk))
|
||||||
|
|
||||||
def test_keyfile_kfenv(self):
|
def test_keyfile_kfenv(self, tmpdir, monkeypatch):
|
||||||
keyfile = os.path.join(self.tmppath2, 'keyfile')
|
keyfile = tmpdir.join('keyfile')
|
||||||
with environment_variable(BORG_KEY_FILE=keyfile, BORG_PASSPHRASE='testkf'):
|
monkeypatch.setenv('BORG_KEY_FILE', str(keyfile))
|
||||||
assert not os.path.exists(keyfile)
|
monkeypatch.setenv('BORG_PASSPHRASE', 'testkf')
|
||||||
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
|
assert not keyfile.exists()
|
||||||
assert os.path.exists(keyfile)
|
key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
|
||||||
chunk = Chunk(b'XXX')
|
assert keyfile.exists()
|
||||||
chunk_id = key.id_hash(chunk.data)
|
chunk = Chunk(b'XXX')
|
||||||
chunk_cdata = key.encrypt(chunk)
|
chunk_id = key.id_hash(chunk.data)
|
||||||
key = KeyfileKey.detect(self.MockRepository(), chunk_cdata)
|
chunk_cdata = key.encrypt(chunk)
|
||||||
self.assert_equal(chunk, key.decrypt(chunk_id, chunk_cdata))
|
key = KeyfileKey.detect(self.MockRepository(), chunk_cdata)
|
||||||
os.unlink(keyfile)
|
assert chunk == key.decrypt(chunk_id, chunk_cdata)
|
||||||
self.assert_raises(FileNotFoundError, KeyfileKey.detect, self.MockRepository(), chunk_cdata)
|
keyfile.remove()
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
KeyfileKey.detect(self.MockRepository(), chunk_cdata)
|
||||||
|
|
||||||
def test_keyfile2(self):
|
def test_keyfile2(self, monkeypatch, keys_dir):
|
||||||
with open(os.path.join(os.environ['BORG_KEYS_DIR'], 'keyfile'), 'w') as fd:
|
with keys_dir.join('keyfile').open('w') as fd:
|
||||||
fd.write(self.keyfile2_key_file)
|
fd.write(self.keyfile2_key_file)
|
||||||
os.environ['BORG_PASSPHRASE'] = 'passphrase'
|
monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
|
||||||
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
|
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
|
||||||
self.assert_equal(key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data, b'payload')
|
assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload'
|
||||||
|
|
||||||
def test_keyfile2_kfenv(self):
|
def test_keyfile2_kfenv(self, tmpdir, monkeypatch):
|
||||||
keyfile = os.path.join(self.tmppath2, 'keyfile')
|
keyfile = tmpdir.join('keyfile')
|
||||||
with open(keyfile, 'w') as fd:
|
with keyfile.open('w') as fd:
|
||||||
fd.write(self.keyfile2_key_file)
|
fd.write(self.keyfile2_key_file)
|
||||||
with environment_variable(BORG_KEY_FILE=keyfile, BORG_PASSPHRASE='passphrase'):
|
monkeypatch.setenv('BORG_KEY_FILE', str(keyfile))
|
||||||
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
|
monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
|
||||||
self.assert_equal(key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data, b'payload')
|
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
|
||||||
|
assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata).data == b'payload'
|
||||||
|
|
||||||
def test_passphrase(self):
|
def test_passphrase(self, keys_dir, monkeypatch):
|
||||||
os.environ['BORG_PASSPHRASE'] = 'test'
|
monkeypatch.setenv('BORG_PASSPHRASE', 'test')
|
||||||
key = PassphraseKey.create(self.MockRepository(), None)
|
key = PassphraseKey.create(self.MockRepository(), None)
|
||||||
self.assert_equal(bytes_to_long(key.enc_cipher.iv, 8), 0)
|
assert bytes_to_long(key.enc_cipher.iv, 8) == 0
|
||||||
self.assert_equal(hexlify(key.id_key), b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6')
|
assert hexlify(key.id_key) == b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6'
|
||||||
self.assert_equal(hexlify(key.enc_hmac_key), b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901')
|
assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901'
|
||||||
self.assert_equal(hexlify(key.enc_key), b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a')
|
assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a'
|
||||||
self.assert_equal(key.chunk_seed, -775740477)
|
assert key.chunk_seed == -775740477
|
||||||
manifest = key.encrypt(Chunk(b'XXX'))
|
manifest = key.encrypt(Chunk(b'XXX'))
|
||||||
self.assert_equal(key.extract_nonce(manifest), 0)
|
assert key.extract_nonce(manifest) == 0
|
||||||
manifest2 = key.encrypt(Chunk(b'XXX'))
|
manifest2 = key.encrypt(Chunk(b'XXX'))
|
||||||
self.assert_not_equal(manifest, manifest2)
|
assert manifest != manifest2
|
||||||
self.assert_equal(key.decrypt(None, manifest), key.decrypt(None, manifest2))
|
assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
|
||||||
self.assert_equal(key.extract_nonce(manifest2), 1)
|
assert key.extract_nonce(manifest2) == 1
|
||||||
iv = key.extract_nonce(manifest)
|
iv = key.extract_nonce(manifest)
|
||||||
key2 = PassphraseKey.detect(self.MockRepository(), manifest)
|
key2 = PassphraseKey.detect(self.MockRepository(), manifest)
|
||||||
self.assert_equal(bytes_to_long(key2.enc_cipher.iv, 8), iv + num_aes_blocks(len(manifest) - PassphraseKey.PAYLOAD_OVERHEAD))
|
assert bytes_to_long(key2.enc_cipher.iv, 8) == iv + num_aes_blocks(len(manifest) - PassphraseKey.PAYLOAD_OVERHEAD)
|
||||||
self.assert_equal(key.id_key, key2.id_key)
|
assert key.id_key == key2.id_key
|
||||||
self.assert_equal(key.enc_hmac_key, key2.enc_hmac_key)
|
assert key.enc_hmac_key == key2.enc_hmac_key
|
||||||
self.assert_equal(key.enc_key, key2.enc_key)
|
assert key.enc_key == key2.enc_key
|
||||||
self.assert_equal(key.chunk_seed, key2.chunk_seed)
|
assert key.chunk_seed == key2.chunk_seed
|
||||||
chunk = Chunk(b'foo')
|
chunk = Chunk(b'foo')
|
||||||
self.assert_equal(hexlify(key.id_hash(chunk.data)), b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990')
|
assert hexlify(key.id_hash(chunk.data)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990'
|
||||||
self.assert_equal(chunk, key2.decrypt(key2.id_hash(chunk.data), key.encrypt(chunk)))
|
assert chunk == key2.decrypt(key2.id_hash(chunk.data), key.encrypt(chunk))
|
||||||
|
|
||||||
|
def _corrupt_byte(self, key, data, offset):
|
||||||
|
data = bytearray(data)
|
||||||
|
data[offset] += 1
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
key.decrypt("", data)
|
||||||
|
|
||||||
|
def test_decrypt_integrity(self, monkeypatch, keys_dir):
|
||||||
|
with keys_dir.join('keyfile').open('w') as fd:
|
||||||
|
fd.write(self.keyfile2_key_file)
|
||||||
|
monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
|
||||||
|
key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
|
||||||
|
|
||||||
|
data = self.keyfile2_cdata
|
||||||
|
for i in range(len(data)):
|
||||||
|
self._corrupt_byte(key, data, i)
|
||||||
|
|
||||||
|
with pytest.raises(IntegrityError):
|
||||||
|
data = bytearray(self.keyfile2_cdata)
|
||||||
|
id = bytearray(key.id_hash(data)) # corrupt chunk id
|
||||||
|
id[12] = 0
|
||||||
|
key.decrypt(id, data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPassphrase:
|
||||||
|
def test_passphrase_new_verification(self, capsys, monkeypatch):
|
||||||
|
monkeypatch.setattr(getpass, 'getpass', lambda prompt: "12aöäü")
|
||||||
|
monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'no')
|
||||||
|
Passphrase.new()
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "12" not in out
|
||||||
|
assert "12" not in err
|
||||||
|
|
||||||
|
monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'yes')
|
||||||
|
passphrase = Passphrase.new()
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "313261c3b6c3a4c3bc" not in out
|
||||||
|
assert "313261c3b6c3a4c3bc" in err
|
||||||
|
assert passphrase == "12aöäü"
|
||||||
|
|
||||||
|
monkeypatch.setattr(getpass, 'getpass', lambda prompt: "1234/@=")
|
||||||
|
Passphrase.new()
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "1234/@=" not in out
|
||||||
|
assert "1234/@=" in err
|
||||||
|
|
||||||
|
def test_passphrase_new_empty(self, capsys, monkeypatch):
|
||||||
|
monkeypatch.delenv('BORG_PASSPHRASE', False)
|
||||||
|
monkeypatch.setattr(getpass, 'getpass', lambda prompt: "")
|
||||||
|
with pytest.raises(PasswordRetriesExceeded):
|
||||||
|
Passphrase.new(allow_empty=False)
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert "must not be blank" in err
|
||||||
|
|
||||||
|
def test_passphrase_new_retries(self, monkeypatch):
|
||||||
|
monkeypatch.delenv('BORG_PASSPHRASE', False)
|
||||||
|
ascending_numbers = iter(range(20))
|
||||||
|
monkeypatch.setattr(getpass, 'getpass', lambda prompt: str(next(ascending_numbers)))
|
||||||
|
with pytest.raises(PasswordRetriesExceeded):
|
||||||
|
Passphrase.new()
|
||||||
|
|
||||||
|
def test_passphrase_repr(self):
|
||||||
|
assert "secret" not in repr(Passphrase("secret"))
|
||||||
|
|
|
@ -4,7 +4,7 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from ..platform import acl_get, acl_set
|
from ..platform import acl_get, acl_set, swidth
|
||||||
from . import BaseTestCase
|
from . import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,3 +138,16 @@ class PlatformDarwinTestCase(BaseTestCase):
|
||||||
self.set_acl(file2.name, b'!#acl 1\ngroup:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:staff:0:allow:read\nuser:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\n', numeric_owner=True)
|
self.set_acl(file2.name, b'!#acl 1\ngroup:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:staff:0:allow:read\nuser:FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000:root:0:allow:read\n', numeric_owner=True)
|
||||||
self.assert_in(b'group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:wheel:0:allow:read', self.get_acl(file2.name)[b'acl_extended'])
|
self.assert_in(b'group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000:wheel:0:allow:read', self.get_acl(file2.name)[b'acl_extended'])
|
||||||
self.assert_in(b'group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read', self.get_acl(file2.name, numeric_owner=True)[b'acl_extended'])
|
self.assert_in(b'group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read', self.get_acl(file2.name, numeric_owner=True)[b'acl_extended'])
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(sys.platform.startswith(('linux', 'freebsd', 'darwin')), 'POSIX only tests')
|
||||||
|
class PlatformPosixTestCase(BaseTestCase):
|
||||||
|
|
||||||
|
def test_swidth_ascii(self):
|
||||||
|
self.assert_equal(swidth("borg"), 4)
|
||||||
|
|
||||||
|
def test_swidth_cjk(self):
|
||||||
|
self.assert_equal(swidth("バックアップ"), 6 * 2)
|
||||||
|
|
||||||
|
def test_swidth_mixed(self):
|
||||||
|
self.assert_equal(swidth("borgバックアップ"), 4 + 6 * 2)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
@ -7,8 +9,8 @@ from unittest.mock import patch
|
||||||
from ..hashindex import NSIndex
|
from ..hashindex import NSIndex
|
||||||
from ..helpers import Location, IntegrityError
|
from ..helpers import Location, IntegrityError
|
||||||
from ..locking import UpgradableLock, LockFailed
|
from ..locking import UpgradableLock, LockFailed
|
||||||
from ..remote import RemoteRepository, InvalidRPCMethod
|
from ..remote import RemoteRepository, InvalidRPCMethod, ConnectionClosedWithHint
|
||||||
from ..repository import Repository
|
from ..repository import Repository, LoggedIO
|
||||||
from . import BaseTestCase
|
from . import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -192,6 +194,13 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase):
|
||||||
self.assert_equal(self.repository.check(), True)
|
self.assert_equal(self.repository.check(), True)
|
||||||
self.assert_equal(len(self.repository), 3)
|
self.assert_equal(len(self.repository), 3)
|
||||||
|
|
||||||
|
def test_ignores_commit_tag_in_data(self):
|
||||||
|
self.repository.put(b'0' * 32, LoggedIO.COMMIT)
|
||||||
|
self.reopen()
|
||||||
|
with self.repository:
|
||||||
|
io = self.repository.io
|
||||||
|
assert not io.is_committed_segment(io.get_latest_segment())
|
||||||
|
|
||||||
|
|
||||||
class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase):
|
class RepositoryAppendOnlyTestCase(RepositoryTestCaseBase):
|
||||||
def test_destroy_append_only(self):
|
def test_destroy_append_only(self):
|
||||||
|
@ -268,7 +277,7 @@ class RepositoryCheckTestCase(RepositoryTestCaseBase):
|
||||||
return set(int(key) for key in self.repository.list())
|
return set(int(key) for key in self.repository.list())
|
||||||
|
|
||||||
def test_repair_corrupted_segment(self):
|
def test_repair_corrupted_segment(self):
|
||||||
self.add_objects([[1, 2, 3], [4, 5, 6]])
|
self.add_objects([[1, 2, 3], [4, 5], [6]])
|
||||||
self.assert_equal(set([1, 2, 3, 4, 5, 6]), self.list_objects())
|
self.assert_equal(set([1, 2, 3, 4, 5, 6]), self.list_objects())
|
||||||
self.check(status=True)
|
self.check(status=True)
|
||||||
self.corrupt_object(5)
|
self.corrupt_object(5)
|
||||||
|
@ -389,3 +398,83 @@ class RemoteRepositoryCheckTestCase(RepositoryCheckTestCase):
|
||||||
def test_crash_before_compact(self):
|
def test_crash_before_compact(self):
|
||||||
# skip this test, we can't mock-patch a Repository class in another process!
|
# skip this test, we can't mock-patch a Repository class in another process!
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteRepositoryLoggingStub(RemoteRepository):
|
||||||
|
""" run a remote command that just prints a logging-formatted message to
|
||||||
|
stderr, and stub out enough of RemoteRepository to avoid the resulting
|
||||||
|
exceptions """
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
self.msg = kw.pop('msg')
|
||||||
|
super().__init__(*args, **kw)
|
||||||
|
|
||||||
|
def borg_cmd(self, cmd, testing):
|
||||||
|
return [sys.executable, '-c', 'import sys; print("{}", file=sys.stderr)'.format(self.msg), ]
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
# clean up from exception without triggering assert
|
||||||
|
if self.p:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteRepositoryLoggerTestCase(RepositoryTestCaseBase):
|
||||||
|
def setUp(self):
|
||||||
|
self.location = Location('__testsuite__:/doesntexist/repo')
|
||||||
|
self.stream = io.StringIO()
|
||||||
|
self.handler = logging.StreamHandler(self.stream)
|
||||||
|
logging.getLogger().handlers[:] = [self.handler]
|
||||||
|
logging.getLogger('borg.repository').handlers[:] = []
|
||||||
|
logging.getLogger('borg.repository.foo').handlers[:] = []
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_repository(self, msg):
|
||||||
|
try:
|
||||||
|
RemoteRepositoryLoggingStub(self.location, msg=msg)
|
||||||
|
except ConnectionClosedWithHint:
|
||||||
|
# stub is dumb, so this exception expected
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_old_format_messages(self):
|
||||||
|
self.handler.setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
self.create_repository("$LOG INFO Remote: old format message")
|
||||||
|
self.assert_equal(self.stream.getvalue(), 'Remote: old format message\n')
|
||||||
|
|
||||||
|
def test_new_format_messages(self):
|
||||||
|
self.handler.setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
self.create_repository("$LOG INFO borg.repository Remote: new format message")
|
||||||
|
self.assert_equal(self.stream.getvalue(), 'Remote: new format message\n')
|
||||||
|
|
||||||
|
def test_remote_messages_screened(self):
|
||||||
|
# default borg config for root logger
|
||||||
|
self.handler.setLevel(logging.WARNING)
|
||||||
|
logging.getLogger().setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
self.create_repository("$LOG INFO borg.repository Remote: new format info message")
|
||||||
|
self.assert_equal(self.stream.getvalue(), '')
|
||||||
|
|
||||||
|
def test_info_to_correct_local_child(self):
|
||||||
|
logging.getLogger('borg.repository').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('borg.repository.foo').setLevel(logging.INFO)
|
||||||
|
# default borg config for root logger
|
||||||
|
self.handler.setLevel(logging.WARNING)
|
||||||
|
logging.getLogger().setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
child_stream = io.StringIO()
|
||||||
|
child_handler = logging.StreamHandler(child_stream)
|
||||||
|
child_handler.setLevel(logging.INFO)
|
||||||
|
logging.getLogger('borg.repository').handlers[:] = [child_handler]
|
||||||
|
foo_stream = io.StringIO()
|
||||||
|
foo_handler = logging.StreamHandler(foo_stream)
|
||||||
|
foo_handler.setLevel(logging.INFO)
|
||||||
|
logging.getLogger('borg.repository.foo').handlers[:] = [foo_handler]
|
||||||
|
|
||||||
|
self.create_repository("$LOG INFO borg.repository Remote: new format child message")
|
||||||
|
self.assert_equal(foo_stream.getvalue(), '')
|
||||||
|
self.assert_equal(child_stream.getvalue(), 'Remote: new format child message\n')
|
||||||
|
self.assert_equal(self.stream.getvalue(), '')
|
||||||
|
|
Loading…
Reference in New Issue