Browse Source

Merge branch 'master' into multithreading

Some tests are failing, some block, one new test needed skipping.
Thomas Waldmann 9 years ago
parent
commit
f4b0f57618
53 changed files with 7899 additions and 1471 deletions
  1. 2 1
      .gitignore
  2. 7 0
      .travis.yml
  3. 4 0
      .travis/install.sh
  4. 2 2
      .travis/run.sh
  5. 7 3
      AUTHORS
  6. 0 510
      CHANGES.rst
  7. 1 0
      CHANGES.rst
  8. 72 24
      README.rst
  9. 5 2
      Vagrantfile
  10. 57 17
      borg/archive.py
  11. 172 128
      borg/archiver.py
  12. 34 10
      borg/cache.py
  13. 11 8
      borg/fuse.py
  14. 122 45
      borg/helpers.py
  15. 17 13
      borg/key.py
  16. 8 5
      borg/locking.py
  17. 85 0
      borg/logger.py
  18. 9 9
      borg/platform_darwin.pyx
  19. 3 3
      borg/platform_freebsd.pyx
  20. 10 10
      borg/platform_linux.pyx
  21. 4 2
      borg/remote.py
  22. 8 5
      borg/repository.py
  23. 3 2
      borg/testsuite/__init__.py
  24. 146 38
      borg/testsuite/archiver.py
  25. 100 0
      borg/testsuite/benchmark.py
  26. 111 30
      borg/testsuite/helpers.py
  27. 40 0
      borg/testsuite/logger.py
  28. 36 0
      borg/testsuite/platform.py
  29. 57 12
      borg/testsuite/upgrader.py
  30. 56 31
      borg/upgrader.py
  31. 1 41
      docs/Makefile
  32. 0 0
      docs/_static/favicon.ico
  33. 0 5
      docs/_themes/local/sidebarlogo.html
  34. 0 20
      docs/_themes/local/sidebarusefullinks.html
  35. 0 173
      docs/_themes/local/static/local.css_t
  36. 0 6
      docs/_themes/local/theme.conf
  37. 11 0
      docs/authors.rst
  38. 575 3
      docs/changes.rst
  39. 10 4
      docs/conf.py
  40. 48 31
      docs/development.rst
  41. 130 75
      docs/faq.rst
  42. 2 2
      docs/global.rst.inc
  43. 3 2
      docs/index.rst
  44. 109 130
      docs/installation.rst
  45. 0 7
      docs/intro.rst
  46. 5550 0
      docs/misc/asciinema/install_and_basics.json
  47. 51 0
      docs/misc/asciinema/install_and_basics.txt
  48. 8 4
      docs/quickstart.rst
  49. 66 39
      docs/usage.rst
  50. 1 0
      requirements.d/development.txt
  51. 2 4
      setup.cfg
  52. 142 14
      setup.py
  53. 1 1
      tox.ini

+ 2 - 1
.gitignore

@@ -2,7 +2,7 @@ MANIFEST
 docs/_build
 docs/_build
 build
 build
 dist
 dist
-env
+borg-env
 .tox
 .tox
 hashindex.c
 hashindex.c
 chunker.c
 chunker.c
@@ -16,6 +16,7 @@ platform_linux.c
 *.pyo
 *.pyo
 *.so
 *.so
 docs/usage/*.inc
 docs/usage/*.inc
+docs/api.rst
 .idea/
 .idea/
 .cache/
 .cache/
 borg/_version.py
 borg/_version.py

+ 7 - 0
.travis.yml

@@ -17,6 +17,9 @@ matrix:
         - python: 3.4
         - python: 3.4
           os: linux
           os: linux
           env: TOXENV=py34
           env: TOXENV=py34
+        - python: 3.5
+          os: linux
+          env: TOXENV=py35
         - language: generic
         - language: generic
           os: osx
           os: osx
           osx_image: xcode6.4
           osx_image: xcode6.4
@@ -29,6 +32,10 @@ matrix:
           os: osx
           os: osx
           osx_image: xcode6.4
           osx_image: xcode6.4
           env: TOXENV=py34
           env: TOXENV=py34
+        - language: generic
+          os: osx
+          osx_image: xcode6.4
+          env: TOXENV=py35
 
 
 install:
 install:
     - ./.travis/install.sh
     - ./.travis/install.sh

+ 4 - 0
.travis/install.sh

@@ -30,6 +30,10 @@ if [[ "$(uname -s)" == 'Darwin' ]]; then
             pyenv install 3.4.3
             pyenv install 3.4.3
             pyenv global 3.4.3
             pyenv global 3.4.3
             ;;
             ;;
+        py35)
+            pyenv install 3.5.0
+            pyenv global 3.5.0
+            ;;
     esac
     esac
     pyenv rehash
     pyenv rehash
     python -m pip install --user virtualenv
     python -m pip install --user virtualenv

+ 2 - 2
.travis/run.sh

@@ -17,7 +17,7 @@ source ~/.venv/bin/activate
 
 
 if [[ "$(uname -s)" == "Darwin" ]]; then
 if [[ "$(uname -s)" == "Darwin" ]]; then
     # no fakeroot on OS X
     # no fakeroot on OS X
-    sudo tox -e $TOXENV
+    sudo tox -e $TOXENV -r
 else
 else
-    fakeroot -u tox
+    fakeroot -u tox -r
 fi
 fi

+ 7 - 3
AUTHORS

@@ -1,10 +1,14 @@
-Borg Developers / Contributors ("The Borg Collective")
-``````````````````````````````````````````````````````
+Contributors ("The Borg Collective")
+====================================
+
 - Thomas Waldmann <tw@waldmann-edv.de>
 - Thomas Waldmann <tw@waldmann-edv.de>
-- Antoine Beaupré
+- Antoine Beaupré <anarcat@debian.org>
 - Radek Podgorny <radek@podgorny.cz>
 - Radek Podgorny <radek@podgorny.cz>
 - Yuri D'Elia
 - Yuri D'Elia
 
 
+Attic authors
+-------------
+
 Borg is a fork of Attic. Attic is written and maintained
 Borg is a fork of Attic. Attic is written and maintained
 by Jonas Borgström and various contributors:
 by Jonas Borgström and various contributors:
 
 

+ 0 - 510
CHANGES.rst

@@ -1,510 +0,0 @@
-Borg Changelog
-==============
-
-Version 0.27.0
---------------
-
-New features:
-
-- "borg upgrade" command - attic -> borg one time converter / migration, #21
-- temporary hack to avoid using lots of disk space for chunks.archive.d, #235:
-  To use it: rm -rf chunks.archive.d ; touch chunks.archive.d
-- respect XDG_CACHE_HOME, attic #181
-- add support for arbitrary SSH commands, attic #99
-- borg delete --cache-only REPO (only delete cache, not REPO), attic #123
-
-
-Bug fixes:
-
-- use Debian 7 (wheezy) to build pyinstaller borgbackup binaries, fixes slow
-  down observed when running the Centos6-built binary on Ubuntu, #222
-- do not crash on empty lock.roster, fixes #232
-- fix multiple issues with the cache config version check, #234
-- fix segment entry header size check, attic #352
-  plus other error handling improvements / code deduplication there.
-- always give segment and offset in repo IntegrityErrors
-
-
-Other changes:
-
-- stop producing binary wheels, remove docs about it, #147
-- docs:
-  - add warning about prune
-  - generate usage include files only as needed
-  - development docs: add Vagrant section
-  - update / improve / reformat FAQ
-  - hint to single-file pyinstaller binaries from README
-
-
-Version 0.26.1
---------------
-
-This is a minor update, just docs and new pyinstaller binaries.
-
-- docs update about python and binary requirements
-- better docs for --read-special, fix #220
-- re-built the binaries, fix #218 and #213 (glibc version issue)
-- update web site about single-file pyinstaller binaries
-
-Note: if you did a python-based installation, there is no need to upgrade.
-
-
-Version 0.26.0
---------------
-
-New features:
-
-- Faster cache sync (do all in one pass, remove tar/compression stuff), #163
-- BORG_REPO env var to specify the default repo, #168
-- read special files as if they were regular files, #79
-- implement borg create --dry-run, attic issue #267
-- Normalize paths before pattern matching on OS X, #143
-- support OpenBSD and NetBSD (except xattrs/ACLs)
-- support / run tests on Python 3.5
-
-Bug fixes:
-
-- borg mount repo: use absolute path, attic #200, attic #137
-- chunker: use off_t to get 64bit on 32bit platform, #178
-- initialize chunker fd to -1, so it's not equal to STDIN_FILENO (0)
-- fix reaction to "no" answer at delete repo prompt, #182
-- setup.py: detect lz4.h header file location
-- to support python < 3.2.4, add less buggy argparse lib from 3.2.6 (#194)
-- fix for obtaining 'char *' from temporary Python value (old code causes
-  a compile error on Mint 17.2)
-- llfuse 0.41 install troubles on some platforms, require < 0.41
-  (UnicodeDecodeError exception due to non-ascii llfuse setup.py)
-- cython code: add some int types to get rid of unspecific python add /
-  subtract operations (avoid undefined symbol FPE_... error on some platforms)
-- fix verbose mode display of stdin backup
-- extract: warn if a include pattern never matched, fixes #209,
-  implement counters for Include/ExcludePatterns
-- archive names with slashes are invalid, attic issue #180
-- chunker: add a check whether the POSIX_FADV_DONTNEED constant is defined -
-  fixes building on OpenBSD.
-
-Other changes:
-
-- detect inconsistency / corruption / hash collision, #170
-- replace versioneer with setuptools_scm, #106
-- docs:
-
-  - pkg-config is needed for llfuse installation
-  - be more clear about pruning, attic issue #132
-- unit tests:
-
-  - xattr: ignore security.selinux attribute showing up
-  - ext3 seems to need a bit more space for a sparse file
-  - do not test lzma level 9 compression (avoid MemoryError)
-  - work around strange mtime granularity issue on netbsd, fixes #204
-  - ignore st_rdev if file is not a block/char device, fixes #203
-  - stay away from the setgid and sticky mode bits
-- use Vagrant to do easy cross-platform testing (#196), currently:
-
-  - Debian 7 "wheezy" 32bit, Debian 8 "jessie" 64bit
-  - Ubuntu 12.04 32bit, Ubuntu 14.04 64bit
-  - Centos 7 64bit
-  - FreeBSD 10.2 64bit
-  - OpenBSD 5.7 64bit
-  - NetBSD 6.1.5 64bit
-  - Darwin (OS X Yosemite)
-
-
-Version 0.25.0
---------------
-
-Compatibility notes:
-
-- lz4 compression library (liblz4) is a new requirement (#156)
-- the new compression code is very compatible: as long as you stay with zlib
-  compression, older borg releases will still be able to read data from a
-  repo/archive made with the new code (note: this is not the case for the
-  default "none" compression, use "zlib,0" if you want a "no compression" mode
-  that can be read by older borg). Also the new code is able to read repos and
-  archives made with older borg versions (for all zlib levels  0..9).
-
-Deprecations:
-
-- --compression N (with N being a number, as in 0.24) is deprecated.
-  We keep the --compression 0..9 for now to not break scripts, but it is
-  deprecated and will be removed later, so better fix your scripts now:
-  --compression 0 (as in 0.24) is the same as --compression zlib,0 (now).
-  BUT: if you do not want compression, you rather want --compression none
-  (which is the default).
-  --compression 1 (in 0.24) is the same as --compression zlib,1 (now)
-  --compression 9 (in 0.24) is the same as --compression zlib,9 (now)
-
-New features:
-
-- create --compression none (default, means: do not compress, just pass through
-  data "as is". this is more efficient than zlib level 0 as used in borg 0.24)
-- create --compression lz4 (super-fast, but not very high compression)
-- create --compression zlib,N (slower, higher compression, default for N is 6)
-- create --compression lzma,N (slowest, highest compression, default N is 6)
-- honor the nodump flag (UF_NODUMP) and do not backup such items
-- list --short just outputs a simple list of the files/directories in an archive
-
-Bug fixes:
-
-- fixed --chunker-params parameter order confusion / malfunction, fixes #154
-- close fds of segments we delete (during compaction)
-- close files which fell out the lrucache
-- fadvise DONTNEED now is only called for the byte range actually read, not for
-  the whole file, fixes #158.
-- fix issue with negative "all archives" size, fixes #165
-- restore_xattrs: ignore if setxattr fails with EACCES, fixes #162
-
-Other changes:
-
-- remove fakeroot requirement for tests, tests run faster without fakeroot
-  (test setup does not fail any more without fakeroot, so you can run with or
-  without fakeroot), fixes #151 and #91.
-- more tests for archiver
-- recover_segment(): don't assume we have an fd for segment
-- lrucache refactoring / cleanup, add dispose function, py.test tests
-- generalize hashindex code for any key length (less hardcoding)
-- lock roster: catch file not found in remove() method and ignore it
-- travis CI: use requirements file
-- improved docs:
-
-  - replace hack for llfuse with proper solution (install libfuse-dev)
-  - update docs about compression
-  - update development docs about fakeroot
-  - internals: add some words about lock files / locking system
-  - support: mention BountySource and for what it can be used
-  - theme: use a lighter green
-  - add pypi, wheel, dist package based install docs
-  - split install docs into system-specific preparations and generic instructions
-
-
-Version 0.24.0
---------------
-
-Incompatible changes (compared to 0.23):
-
-- borg now always issues --umask NNN option when invoking another borg via ssh
-  on the repository server. By that, it's making sure it uses the same umask
-  for remote repos as for local ones. Because of this, you must upgrade both
-  server and client(s) to 0.24.
-- the default umask is 077 now (if you do not specify via --umask) which might
-  be a different one as you used previously. The default umask avoids that
-  you accidentally give access permissions for group and/or others to files
-  created by borg (e.g. the repository).
-
-Deprecations:
-
-- "--encryption passphrase" mode is deprecated, see #85 and #97.
-  See the new "--encryption repokey" mode for a replacement.
-
-New features:
-
-- borg create --chunker-params ... to configure the chunker, fixes #16
-  (attic #302, attic #300, and somehow also #41).
-  This can be used to reduce memory usage caused by chunk management overhead,
-  so borg does not create a huge chunks index/repo index and eats all your RAM
-  if you back up lots of data in huge files (like VM disk images).
-  See docs/misc/create_chunker-params.txt for more information.
-- borg info now reports chunk counts in the chunk index.
-- borg create --compression 0..9 to select zlib compression level, fixes #66
-  (attic #295).
-- borg init --encryption repokey (to store the encryption key into the repo),
-  fixes #85
-- improve at-end error logging, always log exceptions and set exit_code=1
-- LoggedIO: better error checks / exceptions / exception handling
-- implement --remote-path to allow non-default-path borg locations, #125
-- implement --umask M and use 077 as default umask for better security, #117
-- borg check: give a named single archive to it, fixes #139
-- cache sync: show progress indication
-- cache sync: reimplement the chunk index merging in C
-
-Bug fixes:
-
-- fix segfault that happened for unreadable files (chunker: n needs to be a
-  signed size_t), #116
-- fix the repair mode, #144
-- repo delete: add destroy to allowed rpc methods, fixes issue #114
-- more compatible repository locking code (based on mkdir), maybe fixes #92
-  (attic #317, attic #201).
-- better Exception msg if no Borg is installed on the remote repo server, #56
-- create a RepositoryCache implementation that can cope with >2GiB,
-  fixes attic #326.
-- fix Traceback when running check --repair, attic #232
-- clarify help text, fixes #73.
-- add help string for --no-files-cache, fixes #140
-
-Other changes:
-
-- improved docs:
-
-  - added docs/misc directory for misc. writeups that won't be included
-    "as is" into the html docs.
-  - document environment variables and return codes (attic #324, attic #52)
-  - web site: add related projects, fix web site url, IRC #borgbackup
-  - Fedora/Fedora-based install instructions added to docs
-  - Cygwin-based install instructions added to docs
-  - updated AUTHORS
-  - add FAQ entries about redundancy / integrity
-  - clarify that borg extract uses the cwd as extraction target
-  - update internals doc about chunker params, memory usage and compression
-  - added docs about development
-  - add some words about resource usage in general
-  - document how to backup a raw disk
-  - add note about how to run borg from virtual env
-  - add solutions for (ll)fuse installation problems
-  - document what borg check does, fixes #138
-  - reorganize borgbackup.github.io sidebar, prev/next at top
-  - deduplicate and refactor the docs / README.rst
-
-- use borg-tmp as prefix for temporary files / directories
-- short prune options without "keep-" are deprecated, do not suggest them
-- improved tox configuration
-- remove usage of unittest.mock, always use mock from pypi
-- use entrypoints instead of scripts, for better use of the wheel format and
-  modern installs
-- add requirements.d/development.txt and modify tox.ini
-- use travis-ci for testing based on Linux and (new) OS X
-- use coverage.py, pytest-cov and codecov.io for test coverage support
-
-I forgot to list some stuff already implemented in 0.23.0, here they are:
-
-New features:
-
-- efficient archive list from manifest, meaning a big speedup for slow
-  repo connections and "list <repo>", "delete <repo>", "prune" (attic #242,
-  attic #167)
-- big speedup for chunks cache sync (esp. for slow repo connections), fixes #18
-- hashindex: improve error messages
-
-Other changes:
-
-- explicitly specify binary mode to open binary files
-- some easy micro optimizations
-
-
-Version 0.23.0
---------------
-
-Incompatible changes (compared to attic, fork related):
-
-- changed sw name and cli command to "borg", updated docs
-- package name (and name in urls) uses "borgbackup" to have less collisions
-- changed repo / cache internal magic strings from ATTIC* to BORG*,
-  changed cache location to .cache/borg/ - this means that it currently won't
-  accept attic repos (see issue #21 about improving that)
-
-Bug fixes:
-
-- avoid defect python-msgpack releases, fixes attic #171, fixes attic #185
-- fix traceback when trying to do unsupported passphrase change, fixes attic #189
-- datetime does not like the year 10.000, fixes attic #139
-- fix "info" all archives stats, fixes attic #183
-- fix parsing with missing microseconds, fixes attic #282
-- fix misleading hint the fuse ImportError handler gave, fixes attic #237
-- check unpacked data from RPC for tuple type and correct length, fixes attic #127
-- fix Repository._active_txn state when lock upgrade fails
-- give specific path to xattr.is_enabled(), disable symlink setattr call that
-  always fails
-- fix test setup for 32bit platforms, partial fix for attic #196
-- upgraded versioneer, PEP440 compliance, fixes attic #257
-
-New features:
-
-- less memory usage: add global option --no-cache-files
-- check --last N (only check the last N archives)
-- check: sort archives in reverse time order
-- rename repo::oldname newname (rename repository)
-- create -v output more informative
-- create --progress (backup progress indicator)
-- create --timestamp (utc string or reference file/dir)
-- create: if "-" is given as path, read binary from stdin
-- extract: if --stdout is given, write all extracted binary data to stdout
-- extract --sparse (simple sparse file support)
-- extra debug information for 'fread failed'
-- delete <repo> (deletes whole repo + local cache)
-- FUSE: reflect deduplication in allocated blocks
-- only allow whitelisted RPC calls in server mode
-- normalize source/exclude paths before matching
-- use posix_fadvise to not spoil the OS cache, fixes attic #252
-- toplevel error handler: show tracebacks for better error analysis
-- sigusr1 / sigint handler to print current file infos - attic PR #286
-- RPCError: include the exception args we get from remote
-
-Other changes:
-
-- source: misc. cleanups, pep8, style
-- docs and faq improvements, fixes, updates
-- cleanup crypto.pyx, make it easier to adapt to other AES modes
-- do os.fsync like recommended in the python docs
-- source: Let chunker optionally work with os-level file descriptor.
-- source: Linux: remove duplicate os.fsencode calls
-- source: refactor _open_rb code a bit, so it is more consistent / regular
-- source: refactor indicator (status) and item processing
-- source: use py.test for better testing, flake8 for code style checks
-- source: fix tox >=2.0 compatibility (test runner)
-- pypi package: add python version classifiers, add FreeBSD to platforms
-
-
-Attic Changelog
-===============
-
-Here you can see the full list of changes between each Attic release until Borg
-forked from Attic:
-
-Version 0.17
-------------
-
-(bugfix release, released on X)
-- Fix hashindex ARM memory alignment issue (#309)
-- Improve hashindex error messages (#298)
-
-Version 0.16
-------------
-
-(bugfix release, released on May 16, 2015)
-- Fix typo preventing the security confirmation prompt from working (#303)
-- Improve handling of systems with improperly configured file system encoding (#289)
-- Fix "All archives" output for attic info. (#183)
-- More user friendly error message when repository key file is not found (#236)
-- Fix parsing of iso 8601 timestamps with zero microseconds (#282)
-
-Version 0.15
-------------
-
-(bugfix release, released on Apr 15, 2015)
-- xattr: Be less strict about unknown/unsupported platforms (#239)
-- Reduce repository listing memory usage (#163).
-- Fix BrokenPipeError for remote repositories (#233)
-- Fix incorrect behavior with two character directory names (#265, #268)
-- Require approval before accessing relocated/moved repository (#271)
-- Require approval before accessing previously unknown unencrypted repositories (#271)
-- Fix issue with hash index files larger than 2GB.
-- Fix Python 3.2 compatibility issue with noatime open() (#164)
-- Include missing pyx files in dist files (#168)
-
-Version 0.14
-------------
-
-(feature release, released on Dec 17, 2014)
-- Added support for stripping leading path segments (#95)
-  "attic extract --strip-segments X"
-- Add workaround for old Linux systems without acl_extended_file_no_follow (#96)
-- Add MacPorts' path to the default openssl search path (#101)
-- HashIndex improvements, eliminates unnecessary IO on low memory systems.
-- Fix "Number of files" output for attic info. (#124)
-- limit create file permissions so files aren't read while restoring
-- Fix issue with empty xattr values (#106)
-
-Version 0.13
-------------
-
-(feature release, released on Jun 29, 2014)
-
-- Fix sporadic "Resource temporarily unavailable" when using remote repositories
-- Reduce file cache memory usage (#90)
-- Faster AES encryption (utilizing AES-NI when available)
-- Experimental Linux, OS X and FreeBSD ACL support (#66)
-- Added support for backup and restore of BSDFlags (OSX, FreeBSD) (#56)
-- Fix bug where xattrs on symlinks were not correctly restored
-- Added cachedir support. CACHEDIR.TAG compatible cache directories
-  can now be excluded using ``--exclude-caches`` (#74)
-- Fix crash on extreme mtime timestamps (year 2400+) (#81)
-- Fix Python 3.2 specific lockf issue (EDEADLK)
-
-Version 0.12
-------------
-
-(feature release, released on April 7, 2014)
-
-- Python 3.4 support (#62)
-- Various documentation improvements a new style
-- ``attic mount`` now supports mounting an entire repository not only
-  individual archives (#59)
-- Added option to restrict remote repository access to specific path(s):
-  ``attic serve --restrict-to-path X`` (#51)
-- Include "all archives" size information in "--stats" output. (#54)
-- Added ``--stats`` option to ``attic delete`` and ``attic prune``
-- Fixed bug where ``attic prune`` used UTC instead of the local time zone
-  when determining which archives to keep.
-- Switch to SI units (Power of 1000 instead 1024) when printing file sizes
-
-Version 0.11
-------------
-
-(feature release, released on March 7, 2014)
-
-- New "check" command for repository consistency checking (#24)
-- Documentation improvements
-- Fix exception during "attic create" with repeated files (#39)
-- New "--exclude-from" option for attic create/extract/verify.
-- Improved archive metadata deduplication.
-- "attic verify" has been deprecated. Use "attic extract --dry-run" instead.
-- "attic prune --hourly|daily|..." has been deprecated.
-  Use "attic prune --keep-hourly|daily|..." instead.
-- Ignore xattr errors during "extract" if not supported by the filesystem. (#46)
-
-Version 0.10
-------------
-
-(bugfix release, released on Jan 30, 2014)
-
-- Fix deadlock when extracting 0 sized files from remote repositories
-- "--exclude" wildcard patterns are now properly applied to the full path
-  not just the file name part (#5).
-- Make source code endianness agnostic (#1)
-
-Version 0.9
------------
-
-(feature release, released on Jan 23, 2014)
-
-- Remote repository speed and reliability improvements.
-- Fix sorting of segment names to ignore NFS left over files. (#17)
-- Fix incorrect display of time (#13)
-- Improved error handling / reporting. (#12)
-- Use fcntl() instead of flock() when locking repository/cache. (#15)
-- Let ssh figure out port/user if not specified so we don't override .ssh/config (#9)
-- Improved libcrypto path detection (#23).
-
-Version 0.8.1
--------------
-
-(bugfix release, released on Oct 4, 2013)
-
-- Fix segmentation fault issue.
-
-Version 0.8
------------
-
-(feature release, released on Oct 3, 2013)
-
-- Fix xattr issue when backing up sshfs filesystems (#4)
-- Fix issue with excessive index file size (#6)
-- Support access of read only repositories.
-- New syntax to enable repository encryption:
-    attic init --encryption="none|passphrase|keyfile".
-- Detect and abort if repository is older than the cache.
-
-
-Version 0.7
------------
-
-(feature release, released on Aug 5, 2013)
-
-- Ported to FreeBSD
-- Improved documentation
-- Experimental: Archives mountable as fuse filesystems.
-- The "user." prefix is no longer stripped from xattrs on Linux
-
-
-Version 0.6.1
--------------
-
-(bugfix release, released on July 19, 2013)
-
-- Fixed an issue where mtime was not always correctly restored.
-
-
-Version 0.6
------------
-
-First public release on July 9, 2013

+ 1 - 0
CHANGES.rst

@@ -0,0 +1 @@
+docs/changes.rst

+ 72 - 24
README.rst

@@ -1,5 +1,12 @@
+|screencast|
+
+.. |screencast| image:: https://asciinema.org/a/28691.png
+        :alt: BorgBackup Installation and Basic Usage
+        :target: https://asciinema.org/a/28691?autoplay=1&speed=2
+
+
 What is BorgBackup?
 What is BorgBackup?
--------------------
+===================
 BorgBackup (short: Borg) is a deduplicating backup program.
 BorgBackup (short: Borg) is a deduplicating backup program.
 Optionally, it supports compression and authenticated encryption.
 Optionally, it supports compression and authenticated encryption.
 
 
@@ -9,11 +16,13 @@ since only changes are stored.
 The authenticated encryption technique makes it suitable for backups to not
 The authenticated encryption technique makes it suitable for backups to not
 fully trusted targets.
 fully trusted targets.
 
 
-`Borg Installation docs <http://borgbackup.github.io/borgbackup/installation.html>`_
+See the `installation manual`_ or, if you have already
+downloaded Borg, ``docs/installation.rst`` to get started with Borg.
 
 
+.. _installation manual: https://borgbackup.readthedocs.org/installation.html
 
 
 Main features
 Main features
-~~~~~~~~~~~~~
+-------------
 **Space efficient storage**
 **Space efficient storage**
   Deduplication based on content-defined chunking is used to reduce the number
   Deduplication based on content-defined chunking is used to reduce the number
   of bytes stored: each file is split into a number of variable length chunks
   of bytes stored: each file is split into a number of variable length chunks
@@ -63,16 +72,16 @@ Main features
     Backup archives are mountable as userspace filesystems for easy interactive
     Backup archives are mountable as userspace filesystems for easy interactive
     backup examination and restores (e.g. by using a regular file manager).
     backup examination and restores (e.g. by using a regular file manager).
 
 
-**Easy installation**
-    For Linux, Mac OS X and FreeBSD, we offer a single-file pyinstaller binary
-    that does not require installing anything - you can just run it.
+**Easy installation on multiple platforms**
+    We offer single-file binaries
+    that does not require installing anything - you can just run it on
+    the supported platforms:
 
 
-**Platforms Borg works on**
-  * Linux
-  * Mac OS X
-  * FreeBSD
-  * OpenBSD and NetBSD (for both: no xattrs/ACLs support yet)
-  * Cygwin (unsupported)
+    * Linux
+    * Mac OS X
+    * FreeBSD
+    * OpenBSD and NetBSD (no xattrs/ACLs support or binaries yet)
+    * Cygwin (not supported, no binaries yet)
 
 
 **Free and Open Source Software**
 **Free and Open Source Software**
   * security and functionality can be audited independently
   * security and functionality can be audited independently
@@ -80,7 +89,7 @@ Main features
 
 
 
 
 Easy to use
 Easy to use
-~~~~~~~~~~~
+-----------
 Initialize a new backup repository and create a backup archive::
 Initialize a new backup repository and create a backup archive::
 
 
     $ borg init /mnt/backup
     $ borg init /mnt/backup
@@ -88,7 +97,7 @@ Initialize a new backup repository and create a backup archive::
 
 
 Now doing another backup, just to show off the great deduplication::
 Now doing another backup, just to show off the great deduplication::
 
 
-    $ borg create --stats /mnt/backup::Tuesday ~/Documents
+    $ borg create --stats -C zlib,6 /mnt/backup::Tuesday ~/Documents
 
 
     Archive name: Tuesday
     Archive name: Tuesday
     Archive fingerprint: 387a5e3f9b0e792e91c...
     Archive fingerprint: 387a5e3f9b0e792e91c...
@@ -100,29 +109,68 @@ Now doing another backup, just to show off the great deduplication::
     This archive:          57.16 MB           46.78 MB            151.67 kB  <--- !
     This archive:          57.16 MB           46.78 MB            151.67 kB  <--- !
     All archives:         114.02 MB           93.46 MB             44.81 MB
     All archives:         114.02 MB           93.46 MB             44.81 MB
 
 
-For a graphical frontend refer to our complementary project
-`BorgWeb <https://github.com/borgbackup/borgweb>`_.
+For a graphical frontend refer to our complementary project `BorgWeb`_.
+
+Links
+=====
+
+ * `Main Web Site <https://borgbackup.readthedocs.org/>`_
+ * `Releases <https://github.com/borgbackup/borg/releases>`_
+ * `PyPI packages <https://pypi.python.org/pypi/borgbackup>`_
+ * `ChangeLog <https://github.com/borgbackup/borg/blob/master/CHANGES.rst>`_
+ * `GitHub <https://github.com/borgbackup/borg>`_
+ * `Issue Tracker <https://github.com/borgbackup/borg/issues>`_
+ * `Bounties & Fundraisers <https://www.bountysource.com/teams/borgbackup>`_
+ * `Mailing List <http://librelist.com/browser/borgbackup/>`_
+ * `License <https://borgbackup.github.io/borgbackup/authors.html#license>`_
+
+Related Projects
+----------------
 
 
+ * `BorgWeb <https://borgbackup.github.io/borgweb/>`_
+ * `Atticmatic <https://github.com/witten/atticmatic/>`_
+ * `Attic <https://github.com/jborg/attic>`_
 
 
 Notes
 Notes
 -----
 -----
 
 
-Borg is a fork of `Attic <https://github.com/jborg/attic>`_ and maintained by
-"`The Borg Collective <https://github.com/borgbackup/borg/blob/master/AUTHORS>`_".
+Borg is a fork of `Attic`_ and maintained by "`The Borg collective`_".
+
+.. _The Borg collective: https://borgbackup.readthedocs.org/authors.html
+
+Differences between Attic and Borg
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here's a (incomplete) list of some major changes:
+
+ * more open, faster paced development (see `issue #1 <https://github.com/borgbackup/borg/issues/1>`_)
+ * lots of attic issues fixed (see `issue #5 <https://github.com/borgbackup/borg/issues/5>`_)
+ * less chunk management overhead via --chunker-params option (less memory and disk usage)
+ * faster remote cache resync (useful when backing up multiple machines into same repo)
+ * compression: no, lz4, zlib or lzma compression, adjustable compression levels
+ * repokey replaces problematic passphrase mode (you can't change the passphrase nor the pbkdf2 iteration count in "passphrase" mode)
+ * simple sparse file support, great for virtual machine disk files
+ * can read special files (e.g. block devices) or from stdin, write to stdout
+ * mkdir-based locking is more compatible than attic's posix locking
+ * uses fadvise to not spoil / blow up the fs cache
+ * better error messages / exception handling
+ * better output for verbose mode, progress indication
+ * tested on misc. Linux systems, 32 and 64bit, FreeBSD, OpenBSD, NetBSD, Mac OS X
+
+Please read the `ChangeLog`_ (or ``CHANGES.rst`` in the source distribution) for more
+information.
 
 
-Read `issue #1 <https://github.com/borgbackup/borg/issues/1>`_ about the initial
-considerations regarding project goals and policy of the Borg project.
+BORG IS NOT COMPATIBLE WITH ORIGINAL ATTIC (but there is a one-way conversion).
 
 
-BORG IS NOT COMPATIBLE WITH ORIGINAL ATTIC.
 EXPECT THAT WE WILL BREAK COMPATIBILITY REPEATEDLY WHEN MAJOR RELEASE NUMBER
 EXPECT THAT WE WILL BREAK COMPATIBILITY REPEATEDLY WHEN MAJOR RELEASE NUMBER
-CHANGES (like when going from 0.x.y to 1.0.0). Please read CHANGES document.
+CHANGES (like when going from 0.x.y to 1.0.0).
 
 
 NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES.
 NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES.
 
 
 THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS.
 THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS.
 
 
-For more information, please also see the
-`LICENSE  <https://github.com/borgbackup/borg/blob/master/LICENSE>`_.
+Borg is distributed under a 3-clause BSD license, see `License`_
+for the complete license.
 
 
 |build| |coverage|
 |build| |coverage|
 
 

+ 5 - 2
Vagrantfile

@@ -22,6 +22,7 @@ def packages_debianoid
     apt-get update
     apt-get update
     # for building borgbackup and dependencies:
     # for building borgbackup and dependencies:
     apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config
     apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config
+    usermod -a -G fuse vagrant
     apt-get install -y fakeroot build-essential git
     apt-get install -y fakeroot build-essential git
     apt-get install -y python3-dev python3-setuptools
     apt-get install -y python3-dev python3-setuptools
     # for building python:
     # for building python:
@@ -137,7 +138,7 @@ end
 def install_pyenv(boxname)
 def install_pyenv(boxname)
   return <<-EOF
   return <<-EOF
     curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash
     curl -s -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash
-    echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bash_profile
+    echo 'export PATH="$HOME/.pyenv/bin:/vagrant/borg:$PATH"' >> ~/.bash_profile
     echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
     echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
     echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile
     echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile
     echo 'export PYTHON_CONFIGURE_OPTS="--enable-shared"' >> ~/.bash_profile
     echo 'export PYTHON_CONFIGURE_OPTS="--enable-shared"' >> ~/.bash_profile
@@ -232,7 +233,7 @@ def build_binary_with_pyinstaller(boxname)
     cd /vagrant/borg
     cd /vagrant/borg
     . borg-env/bin/activate
     . borg-env/bin/activate
     cd borg
     cd borg
-    pyinstaller -F -n borg --hidden-import=logging.config borg/__main__.py
+    pyinstaller -F -n borg.exe --distpath=/vagrant/borg --clean --hidden-import=logging.config borg/__main__.py
   EOF
   EOF
 end
 end
 
 
@@ -247,8 +248,10 @@ def run_tests(boxname)
     fi
     fi
     # otherwise: just use the system python
     # otherwise: just use the system python
     if which fakeroot > /dev/null; then
     if which fakeroot > /dev/null; then
+      echo "Running tox WITH fakeroot -u"
       fakeroot -u tox --skip-missing-interpreters
       fakeroot -u tox --skip-missing-interpreters
     else
     else
+      echo "Running tox WITHOUT fakeroot -u"
       tox --skip-missing-interpreters
       tox --skip-missing-interpreters
     fi
     fi
   EOF
   EOF

+ 57 - 17
borg/archive.py

@@ -1,11 +1,16 @@
+from binascii import hexlify
 from datetime import datetime
 from datetime import datetime
 from getpass import getuser
 from getpass import getuser
 from itertools import groupby
 from itertools import groupby
 import errno
 import errno
 import threading
 import threading
+import logging
+
+from .logger import create_logger
+logger = create_logger()
+
 from .key import key_factory
 from .key import key_factory
 from .remote import cache_if_remote
 from .remote import cache_if_remote
-import msgpack
 from multiprocessing import cpu_count
 from multiprocessing import cpu_count
 import os
 import os
 import socket
 import socket
@@ -14,12 +19,17 @@ import sys
 import time
 import time
 from io import BytesIO
 from io import BytesIO
 from . import xattr
 from . import xattr
-from .platform import acl_get, acl_set
-from .chunker import Chunker
-from .hashindex import ChunkIndex
-from .helpers import parse_timestamp, Error, uid2user, user2uid, gid2group, group2gid, \
-    Manifest, Statistics, decode_dict, st_mtime_ns, make_path_safe, StableDict, int_to_bigint, bigint_to_int, \
-    make_queue, TerminatedQueue
+from .helpers import parse_timestamp, Error, uid2user, user2uid, gid2group, group2gid, format_timedelta, \
+    Manifest, Statistics, decode_dict, make_path_safe, StableDict, int_to_bigint, bigint_to_int, have_cython, \
+    st_atime_ns, st_ctime_ns, st_mtime_ns, make_queue, TerminatedQueue
+if have_cython():
+    from .platform import acl_get, acl_set
+    from .chunker import Chunker
+    from .hashindex import ChunkIndex
+    import msgpack
+else:
+    import mock
+    msgpack = mock.Mock()
 
 
 ITEMS_BUFFER = 1024 * 1024
 ITEMS_BUFFER = 1024 * 1024
 
 
@@ -317,7 +327,8 @@ class Archive:
 
 
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
                  checkpoint_interval=300, numeric_owner=False, progress=False,
                  checkpoint_interval=300, numeric_owner=False, progress=False,
-                 chunker_params=CHUNKER_PARAMS):
+                 chunker_params=CHUNKER_PARAMS,
+                 start=datetime.now(), end=datetime.now()):
         self.cwd = os.getcwd()
         self.cwd = os.getcwd()
         self.key = key
         self.key = key
         self.repository = repository
         self.repository = repository
@@ -330,6 +341,8 @@ class Archive:
         self.name = name
         self.name = name
         self.checkpoint_interval = checkpoint_interval
         self.checkpoint_interval = checkpoint_interval
         self.numeric_owner = numeric_owner
         self.numeric_owner = numeric_owner
+        self.start = start
+        self.end = end
         self.pipeline = DownloadPipeline(self.repository, self.key)
         self.pipeline = DownloadPipeline(self.repository, self.key)
         if create:
         if create:
             self.pp = ParallelProcessor(self)
             self.pp = ParallelProcessor(self)
@@ -375,6 +388,22 @@ class Archive:
         """Timestamp of archive creation in UTC"""
         """Timestamp of archive creation in UTC"""
         return parse_timestamp(self.metadata[b'time'])
         return parse_timestamp(self.metadata[b'time'])
 
 
+    @property
+    def fpr(self):
+        return hexlify(self.id).decode('ascii')
+
+    @property
+    def duration(self):
+        return format_timedelta(self.end-self.start)
+
+    def __str__(self):
+        return '''Archive name: {0.name}
+Archive fingerprint: {0.fpr}
+Start time: {0.start:%c}
+End time: {0.end:%c}
+Duration: {0.duration}
+Number of files: {0.stats.nfiles}'''.format(self)
+
     def __repr__(self):
     def __repr__(self):
         return 'Archive(%r)' % self.name
         return 'Archive(%r)' % self.name
 
 
@@ -565,12 +594,17 @@ class Archive:
         elif has_lchmod:  # Not available on Linux
         elif has_lchmod:  # Not available on Linux
             os.lchmod(path, item[b'mode'])
             os.lchmod(path, item[b'mode'])
         mtime = bigint_to_int(item[b'mtime'])
         mtime = bigint_to_int(item[b'mtime'])
+        if b'atime' in item:
+            atime = bigint_to_int(item[b'atime'])
+        else:
+            # old archives only had mtime in item metadata
+            atime = mtime
         if fd and utime_supports_fd:  # Python >= 3.3
         if fd and utime_supports_fd:  # Python >= 3.3
-            os.utime(fd, None, ns=(mtime, mtime))
+            os.utime(fd, None, ns=(atime, mtime))
         elif utime_supports_follow_symlinks:  # Python >= 3.3
         elif utime_supports_follow_symlinks:  # Python >= 3.3
-            os.utime(path, None, ns=(mtime, mtime), follow_symlinks=False)
+            os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)
         elif not symlink:
         elif not symlink:
-            os.utime(path, (mtime / 1e9, mtime / 1e9))
+            os.utime(path, (atime / 1e9, mtime / 1e9))
         acl_set(path, item, self.numeric_owner)
         acl_set(path, item, self.numeric_owner)
         # Only available on OS X and FreeBSD
         # Only available on OS X and FreeBSD
         if has_lchflags and b'bsdflags' in item:
         if has_lchflags and b'bsdflags' in item:
@@ -609,7 +643,9 @@ class Archive:
             b'mode': st.st_mode,
             b'mode': st.st_mode,
             b'uid': st.st_uid, b'user': uid2user(st.st_uid),
             b'uid': st.st_uid, b'user': uid2user(st.st_uid),
             b'gid': st.st_gid, b'group': gid2group(st.st_gid),
             b'gid': st.st_gid, b'group': gid2group(st.st_gid),
-            b'mtime': int_to_bigint(st_mtime_ns(st))
+            b'atime': int_to_bigint(st_atime_ns(st)),
+            b'ctime': int_to_bigint(st_ctime_ns(st)),
+            b'mtime': int_to_bigint(st_mtime_ns(st)),
         }
         }
         if self.numeric_owner:
         if self.numeric_owner:
             item[b'user'] = item[b'group'] = None
             item[b'user'] = item[b'group'] = None
@@ -677,7 +713,10 @@ class Archive:
             else:
             else:
                 self.hard_links[st.st_ino, st.st_dev] = safe_path
                 self.hard_links[st.st_ino, st.st_dev] = safe_path
         path_hash = self.key.id_hash(os.path.join(self.cwd, path).encode('utf-8', 'surrogateescape'))
         path_hash = self.key.id_hash(os.path.join(self.cwd, path).encode('utf-8', 'surrogateescape'))
+        first_run = not cache.files
         ids = cache.file_known_and_unchanged(path_hash, st)
         ids = cache.file_known_and_unchanged(path_hash, st)
+        if first_run:
+            logger.info('processing files')
         chunks = None
         chunks = None
         if ids is not None:
         if ids is not None:
             # Make sure all ids are available
             # Make sure all ids are available
@@ -713,7 +752,7 @@ class Archive:
     @staticmethod
     @staticmethod
     def _open_rb(path, st):
     def _open_rb(path, st):
         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, 'NO_ATIME', 0)
+        flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
         euid = None
         euid = None
 
 
         def open_simple(p, s):
         def open_simple(p, s):
@@ -833,7 +872,7 @@ class ArchiveChecker:
         self.orphan_chunks_check()
         self.orphan_chunks_check()
         self.finish()
         self.finish()
         if not self.error_found:
         if not self.error_found:
-            self.report_progress('Archive consistency check complete, no problems found.')
+            logger.info('Archive consistency check complete, no problems found.')
         return self.repair or not self.error_found
         return self.repair or not self.error_found
 
 
     def init_chunks(self):
     def init_chunks(self):
@@ -855,7 +894,7 @@ class ArchiveChecker:
     def report_progress(self, msg, error=False):
     def report_progress(self, msg, error=False):
         if error:
         if error:
             self.error_found = True
             self.error_found = True
-        print(msg, file=sys.stderr if error else sys.stdout)
+        logger.log(logging.ERROR if error else logging.WARNING, msg)
 
 
     def identify_key(self, repository):
     def identify_key(self, repository):
         cdata = repository.get(next(self.chunks.iteritems())[0])
         cdata = repository.get(next(self.chunks.iteritems())[0])
@@ -982,7 +1021,7 @@ class ArchiveChecker:
             num_archives = 1
             num_archives = 1
             end = 1
             end = 1
         for i, (name, info) in enumerate(archive_items[:end]):
         for i, (name, info) in enumerate(archive_items[:end]):
-            self.report_progress('Analyzing archive {} ({}/{})'.format(name, num_archives - i, num_archives))
+            logger.info('Analyzing archive {} ({}/{})'.format(name, num_archives - i, num_archives))
             archive_id = info[b'id']
             archive_id = info[b'id']
             if archive_id not in self.chunks:
             if archive_id not in self.chunks:
                 self.report_progress('Archive metadata block is missing', error=True)
                 self.report_progress('Archive metadata block is missing', error=True)
@@ -994,7 +1033,8 @@ class ArchiveChecker:
             archive = StableDict(msgpack.unpackb(data))
             archive = StableDict(msgpack.unpackb(data))
             if archive[b'version'] != 1:
             if archive[b'version'] != 1:
                 raise Exception('Unknown archive metadata version')
                 raise Exception('Unknown archive metadata version')
-            decode_dict(archive, (b'name', b'hostname', b'username', b'time'))  # fixme: argv
+            decode_dict(archive, (b'name', b'hostname', b'username', b'time'))
+            archive[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in archive[b'cmdline']]
             items_buffer = ChunkBuffer(self.key)
             items_buffer = ChunkBuffer(self.key)
             items_buffer.write_chunk = add_callback
             items_buffer.write_chunk = add_callback
             for item in robust_iterator(archive):
             for item in robust_iterator(archive):

+ 172 - 128
borg/archiver.py

@@ -15,17 +15,21 @@ import textwrap
 import traceback
 import traceback
 
 
 from . import __version__
 from . import __version__
-from .archive import Archive, ArchiveChecker, CHUNKER_PARAMS
-from .compress import Compressor, COMPR_BUFFER
-from .upgrader import AtticRepositoryUpgrader
-from .repository import Repository
-from .cache import Cache
-from .key import key_creator
 from .helpers import Error, location_validator, format_time, format_file_size, \
 from .helpers import Error, location_validator, format_time, format_file_size, \
     format_file_mode, ExcludePattern, IncludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
     format_file_mode, ExcludePattern, IncludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
     get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
     get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
     Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
     Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
-    is_cachedir, bigint_to_int, ChunkerParams, CompressionSpec
+    is_cachedir, bigint_to_int, ChunkerParams, CompressionSpec, have_cython, \
+    EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR
+from .logger import create_logger, setup_logging
+logger = create_logger()
+if have_cython():
+    from .compress import Compressor, COMPR_BUFFER
+    from .upgrader import AtticRepositoryUpgrader
+    from .repository import Repository
+    from .cache import Cache
+    from .key import key_creator
+from .archive import Archive, ArchiveChecker, CHUNKER_PARAMS
 from .remote import RepositoryServer, RemoteRepository
 from .remote import RepositoryServer, RemoteRepository
 
 
 has_lchflags = hasattr(os, 'lchflags')
 has_lchflags = hasattr(os, 'lchflags')
@@ -33,8 +37,9 @@ has_lchflags = hasattr(os, 'lchflags')
 
 
 class Archiver:
 class Archiver:
 
 
-    def __init__(self):
-        self.exit_code = 0
+    def __init__(self, verbose=False):
+        self.exit_code = EXIT_SUCCESS
+        self.verbose = verbose
 
 
     def open_repository(self, location, create=False, exclusive=False):
     def open_repository(self, location, create=False, exclusive=False):
         if location.proto == 'ssh':
         if location.proto == 'ssh':
@@ -46,16 +51,21 @@ class Archiver:
 
 
     def print_error(self, msg, *args):
     def print_error(self, msg, *args):
         msg = args and msg % args or msg
         msg = args and msg % args or msg
-        self.exit_code = 1
-        print('borg: ' + msg, file=sys.stderr)
+        self.exit_code = EXIT_ERROR
+        logger.error(msg)
+
+    def print_warning(self, msg, *args):
+        msg = args and msg % args or msg
+        self.exit_code = EXIT_WARNING  # we do not terminate here, so it is a warning
+        logger.warning(msg)
 
 
-    def print_verbose(self, msg, *args, **kw):
+    def print_info(self, msg, *args):
         if self.verbose:
         if self.verbose:
             msg = args and msg % args or msg
             msg = args and msg % args or msg
-            if kw.get('newline', True):
-                print(msg)
-            else:
-                print(msg, end=' ')
+            logger.info(msg)
+
+    def print_status(self, status, path):
+        self.print_info("%1s %s", status, remove_surrogates(path))
 
 
     def do_serve(self, args):
     def do_serve(self, args):
         """Start in server mode. This command is usually not used manually.
         """Start in server mode. This command is usually not used manually.
@@ -64,7 +74,7 @@ class Archiver:
 
 
     def do_init(self, args):
     def do_init(self, args):
         """Initialize an empty repository"""
         """Initialize an empty repository"""
-        print('Initializing repository at "%s"' % args.repository.orig)
+        logger.info('Initializing repository at "%s"' % args.repository.orig)
         repository = self.open_repository(args.repository, create=True, exclusive=True)
         repository = self.open_repository(args.repository, create=True, exclusive=True)
         key = key_creator(repository, args)
         key = key_creator(repository, args)
         manifest = Manifest(key, repository)
         manifest = Manifest(key, repository)
@@ -79,29 +89,29 @@ class Archiver:
         repository = self.open_repository(args.repository, exclusive=args.repair)
         repository = self.open_repository(args.repository, exclusive=args.repair)
         if args.repair:
         if args.repair:
             while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
             while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
-                self.print_error("""Warning: 'check --repair' is an experimental feature that might result
+                self.print_warning("""'check --repair' is an experimental feature that might result
 in data loss.
 in data loss.
 
 
 Type "Yes I am sure" if you understand this and want to continue.\n""")
 Type "Yes I am sure" if you understand this and want to continue.\n""")
                 if input('Do you want to continue? ') == 'Yes I am sure':
                 if input('Do you want to continue? ') == 'Yes I am sure':
                     break
                     break
         if not args.archives_only:
         if not args.archives_only:
-            print('Starting repository check...')
+            logger.info('Starting repository check...')
             if repository.check(repair=args.repair):
             if repository.check(repair=args.repair):
-                print('Repository check complete, no problems found.')
+                logger.info('Repository check complete, no problems found.')
             else:
             else:
-                return 1
+                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.repository.archive, last=args.last):
                 repository, repair=args.repair, archive=args.repository.archive, last=args.last):
-            return 1
-        return 0
+            return EXIT_WARNING
+        return EXIT_SUCCESS
 
 
     def do_change_passphrase(self, args):
     def do_change_passphrase(self, args):
         """Change repository key file passphrase"""
         """Change repository key file passphrase"""
         repository = self.open_repository(args.repository)
         repository = self.open_repository(args.repository)
         manifest, key = Manifest.load(repository)
         manifest, key = Manifest.load(repository)
         key.change_passphrase()
         key.change_passphrase()
-        return 0
+        return EXIT_SUCCESS
 
 
     def do_create(self, args):
     def do_create(self, args):
         """Create new archive"""
         """Create new archive"""
@@ -117,7 +127,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
             archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
                               create=True, checkpoint_interval=args.checkpoint_interval,
                               create=True, checkpoint_interval=args.checkpoint_interval,
                               numeric_owner=args.numeric_owner, progress=args.progress,
                               numeric_owner=args.numeric_owner, progress=args.progress,
-                              chunker_params=args.chunker_params)
+                              chunker_params=args.chunker_params, start=t0)
         else:
         else:
             archive = cache = None
             archive = cache = None
         try:
         try:
@@ -142,17 +152,18 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                         try:
                         try:
                             status = archive.process_stdin(path, cache)
                             status = archive.process_stdin(path, cache)
                         except IOError as e:
                         except IOError as e:
-                            self.print_error('%s: %s', path, e)
+                            status = 'E'
+                            self.print_warning('%s: %s', path, e)
                     else:
                     else:
                         status = '-'
                         status = '-'
-                    self.print_verbose("%1s %s", status, path)
+                    self.print_status(status, path)
                     continue
                     continue
                 path = os.path.normpath(path)
                 path = os.path.normpath(path)
-                if args.dontcross:
+                if args.one_file_system:
                     try:
                     try:
                         restrict_dev = os.lstat(path).st_dev
                         restrict_dev = os.lstat(path).st_dev
                     except OSError as e:
                     except OSError as e:
-                        self.print_error('%s: %s', path, e)
+                        self.print_warning('%s: %s', path, e)
                         continue
                         continue
                 else:
                 else:
                     restrict_dev = None
                     restrict_dev = None
@@ -163,16 +174,12 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 if args.progress:
                 if args.progress:
                     archive.stats.show_progress(final=True)
                     archive.stats.show_progress(final=True)
                 if args.stats:
                 if args.stats:
-                    t = datetime.now()
-                    diff = t - t0
+                    archive.end = datetime.now()
                     print('-' * 78)
                     print('-' * 78)
-                    print('Archive name: %s' % args.archive.archive)
-                    print('Archive fingerprint: %s' % hexlify(archive.id).decode('ascii'))
-                    print('Start time: %s' % t0.strftime('%c'))
-                    print('End time: %s' % t.strftime('%c'))
-                    print('Duration: %s' % format_timedelta(diff))
-                    print('Number of files: %d' % archive.stats.nfiles)
-                    archive.stats.print_('This archive:', cache)
+                    print(str(archive))
+                    print()
+                    print(str(archive.stats))
+                    print(str(cache))
                     print('-' * 78)
                     print('-' * 78)
         finally:
         finally:
             if not dry_run:
             if not dry_run:
@@ -186,7 +193,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         try:
         try:
             st = os.lstat(path)
             st = os.lstat(path)
         except OSError as e:
         except OSError as e:
-            self.print_error('%s: %s', path, e)
+            self.print_warning('%s: %s', path, e)
             return
             return
         if (st.st_ino, st.st_dev) in skip_inodes:
         if (st.st_ino, st.st_dev) in skip_inodes:
             return
             return
@@ -203,7 +210,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 try:
                 try:
                     status = archive.process_file(path, st, cache)
                     status = archive.process_file(path, st, cache)
                 except IOError as e:
                 except IOError as e:
-                    self.print_error('%s: %s', path, e)
+                    status = 'E'
+                    self.print_warning('%s: %s', path, e)
         elif stat.S_ISDIR(st.st_mode):
         elif stat.S_ISDIR(st.st_mode):
             if exclude_caches and is_cachedir(path):
             if exclude_caches and is_cachedir(path):
                 return
                 return
@@ -212,7 +220,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             try:
             try:
                 entries = os.listdir(path)
                 entries = os.listdir(path)
             except OSError as e:
             except OSError as e:
-                self.print_error('%s: %s', path, e)
+                status = 'E'
+                self.print_warning('%s: %s', path, e)
             else:
             else:
                 for filename in sorted(entries):
                 for filename in sorted(entries):
                     entry_path = os.path.normpath(os.path.join(path, filename))
                     entry_path = os.path.normpath(os.path.join(path, filename))
@@ -232,7 +241,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             # Ignore unix sockets
             # Ignore unix sockets
             return
             return
         else:
         else:
-            self.print_error('Unknown file type: %s', path)
+            self.print_warning('Unknown file type: %s', path)
             return
             return
         # Status output
         # Status output
         # A lowercase character means a file type other than a regular file,
         # A lowercase character means a file type other than a regular file,
@@ -249,13 +258,13 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 status = '-'  # dry run, item was not backed up
                 status = '-'  # dry run, item was not backed up
         # output ALL the stuff - it can be easily filtered using grep.
         # output ALL the stuff - it can be easily filtered using grep.
         # even stuff considered unchanged might be interesting.
         # even stuff considered unchanged might be interesting.
-        self.print_verbose("%1s %s", status, remove_surrogates(path))
+        self.print_status(status, path)
 
 
     def do_extract(self, args):
     def do_extract(self, args):
         """Extract archive contents"""
         """Extract archive contents"""
         # be restrictive when restoring files, restore permissions later
         # be restrictive when restoring files, restore permissions later
         if sys.getfilesystemencoding() == 'ascii':
         if sys.getfilesystemencoding() == 'ascii':
-            print('Warning: File system encoding is "ascii", extracting non-ascii filenames will not be supported.')
+            logger.warning('Warning: File system encoding is "ascii", extracting non-ascii filenames will not be supported.')
         repository = self.open_repository(args.archive)
         repository = self.open_repository(args.archive)
         manifest, key = Manifest.load(repository)
         manifest, key = Manifest.load(repository)
         archive = Archive(repository, key, manifest, args.archive.archive,
         archive = Archive(repository, key, manifest, args.archive.archive,
@@ -272,7 +281,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 item[b'path'] = os.sep.join(orig_path.split(os.sep)[strip_components:])
                 item[b'path'] = os.sep.join(orig_path.split(os.sep)[strip_components:])
                 if not item[b'path']:
                 if not item[b'path']:
                     continue
                     continue
-            self.print_verbose(remove_surrogates(orig_path))
+            self.print_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)
@@ -283,7 +292,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                     else:
                     else:
                         archive.extract_item(item, stdout=stdout, sparse=sparse)
                         archive.extract_item(item, stdout=stdout, sparse=sparse)
             except IOError as e:
             except IOError as e:
-                self.print_error('%s: %s', remove_surrogates(orig_path), e)
+                self.print_warning('%s: %s', remove_surrogates(orig_path), e)
 
 
         if not args.dry_run:
         if not args.dry_run:
             # need to set each directory's timestamps AFTER all files in it are
             # need to set each directory's timestamps AFTER all files in it are
@@ -293,7 +302,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 archive.extract_item(dirs.pop(-1))
                 archive.extract_item(dirs.pop(-1))
         for pattern in (patterns or []):
         for pattern in (patterns or []):
             if isinstance(pattern, IncludePattern) and  pattern.match_count == 0:
             if isinstance(pattern, IncludePattern) and  pattern.match_count == 0:
-                self.print_error("Warning: Include pattern '%s' never matched.", pattern)
+                self.print_warning("Include pattern '%s' never matched.", pattern)
         return self.exit_code
         return self.exit_code
 
 
     def do_rename(self, args):
     def do_rename(self, args):
@@ -321,21 +330,23 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             repository.commit()
             repository.commit()
             cache.commit()
             cache.commit()
             if args.stats:
             if args.stats:
-                stats.print_('Deleted data:', cache)
+                logger.info(stats.summary.format(label='Deleted data:', stats=stats))
+                logger.info(str(cache))
         else:
         else:
             if not args.cache_only:
             if not args.cache_only:
-                print("You requested to completely DELETE the repository *including* all archives it contains:")
+                print("You requested to completely DELETE the repository *including* all archives it contains:", file=sys.stderr)
                 for archive_info in manifest.list_archive_infos(sort_by='ts'):
                 for archive_info in manifest.list_archive_infos(sort_by='ts'):
-                    print(format_archive(archive_info))
+                    print(format_archive(archive_info), file=sys.stderr)
                 if not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
                 if not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
-                    print("""Type "YES" if you understand this and want to continue.\n""")
+                    print("""Type "YES" if you understand this and want to continue.\n""", file=sys.stderr)
+                    # XXX: prompt may end up on stdout, but we'll assume that input() does the right thing
                     if input('Do you want to continue? ') != 'YES':
                     if input('Do you want to continue? ') != 'YES':
-                        self.exit_code = 1
+                        self.exit_code = EXIT_ERROR
                         return self.exit_code
                         return self.exit_code
                 repository.destroy()
                 repository.destroy()
-                print("Repository deleted.")
+                logger.info("Repository deleted.")
             cache.destroy()
             cache.destroy()
-            print("Cache deleted.")
+            logger.info("Cache deleted.")
         return self.exit_code
         return self.exit_code
 
 
     def do_mount(self, args):
     def do_mount(self, args):
@@ -343,7 +354,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         try:
         try:
             from .fuse import FuseOperations
             from .fuse import FuseOperations
         except ImportError as e:
         except ImportError as e:
-            self.print_error('loading fuse support failed [ImportError: %s]' % str(e))
+            self.print_error('Loading fuse support failed [ImportError: %s]' % str(e))
             return self.exit_code
             return self.exit_code
 
 
         if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
         if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
@@ -351,18 +362,21 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             return self.exit_code
             return self.exit_code
 
 
         repository = self.open_repository(args.src)
         repository = self.open_repository(args.src)
-        manifest, key = Manifest.load(repository)
-        if args.src.archive:
-            archive = Archive(repository, key, manifest, args.src.archive)
-        else:
-            archive = None
-        operations = FuseOperations(key, repository, manifest, archive)
-        self.print_verbose("Mounting filesystem")
         try:
         try:
-            operations.mount(args.mountpoint, args.options, args.foreground)
-        except RuntimeError:
-            # Relevant error message already printed to stderr by fuse
-            self.exit_code = 1
+            manifest, key = Manifest.load(repository)
+            if args.src.archive:
+                archive = Archive(repository, key, manifest, args.src.archive)
+            else:
+                archive = None
+            operations = FuseOperations(key, repository, manifest, archive)
+            self.print_info("Mounting filesystem")
+            try:
+                operations.mount(args.mountpoint, args.options, args.foreground)
+            except RuntimeError:
+                # Relevant error message already printed to stderr by fuse
+                self.exit_code = EXIT_ERROR
+        finally:
+            repository.close()
         return self.exit_code
         return self.exit_code
 
 
     def do_list(self, args):
     def do_list(self, args):
@@ -421,7 +435,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         print('Time: %s' % to_localtime(archive.ts).strftime('%c'))
         print('Time: %s' % to_localtime(archive.ts).strftime('%c'))
         print('Command line:', remove_surrogates(' '.join(archive.metadata[b'cmdline'])))
         print('Command line:', remove_surrogates(' '.join(archive.metadata[b'cmdline'])))
         print('Number of files: %d' % stats.nfiles)
         print('Number of files: %d' % stats.nfiles)
-        stats.print_('This archive:', cache)
+        print()
+        print(str(stats))
+        print(str(cache))
         return self.exit_code
         return self.exit_code
 
 
     def do_prune(self, args):
     def do_prune(self, args):
@@ -433,7 +449,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
         if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
             self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
             self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
                              '"keep-monthly" or "keep-yearly" settings must be specified')
                              '"keep-monthly" or "keep-yearly" settings must be specified')
-            return 1
+            return self.exit_code
         if args.prefix:
         if args.prefix:
             archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
             archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
         keep = []
         keep = []
@@ -454,19 +470,20 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         to_delete = [a for a in archives if a not in keep]
         to_delete = [a for a in archives if a not in keep]
         stats = Statistics()
         stats = Statistics()
         for archive in keep:
         for archive in keep:
-            self.print_verbose('Keeping archive: %s' % format_archive(archive))
+            self.print_info('Keeping archive: %s' % format_archive(archive))
         for archive in to_delete:
         for archive in to_delete:
             if args.dry_run:
             if args.dry_run:
-                self.print_verbose('Would prune:     %s' % format_archive(archive))
+                self.print_info('Would prune:     %s' % format_archive(archive))
             else:
             else:
-                self.print_verbose('Pruning archive: %s' % format_archive(archive))
+                self.print_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)
         if to_delete and not args.dry_run:
         if to_delete and not args.dry_run:
             manifest.write()
             manifest.write()
             repository.commit()
             repository.commit()
             cache.commit()
             cache.commit()
         if args.stats:
         if args.stats:
-            stats.print_('Deleted data:', cache)
+            logger.info(stats.summary.format(label='Deleted data:', stats=stats))
+            logger.info(str(cache))
         return self.exit_code
         return self.exit_code
 
 
     def do_upgrade(self, args):
     def do_upgrade(self, args):
@@ -482,7 +499,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         # XXX: should auto-detect if it is an attic repository here
         # XXX: should auto-detect if it is an attic repository here
         repo = AtticRepositoryUpgrader(args.repository.path, create=False)
         repo = AtticRepositoryUpgrader(args.repository.path, create=False)
         try:
         try:
-            repo.upgrade(args.dry_run)
+            repo.upgrade(args.dry_run, inplace=args.inplace)
         except NotImplementedError as e:
         except NotImplementedError as e:
             print("warning: %s" % e)
             print("warning: %s" % e)
         return self.exit_code
         return self.exit_code
@@ -540,7 +557,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
             ('--daily', '--keep-daily', 'Warning: "--daily" has been deprecated. Use "--keep-daily" instead.'),
             ('--daily', '--keep-daily', 'Warning: "--daily" has been deprecated. Use "--keep-daily" instead.'),
             ('--weekly', '--keep-weekly', 'Warning: "--weekly" has been deprecated. Use "--keep-weekly" instead.'),
             ('--weekly', '--keep-weekly', 'Warning: "--weekly" has been deprecated. Use "--keep-weekly" instead.'),
             ('--monthly', '--keep-monthly', 'Warning: "--monthly" has been deprecated. Use "--keep-monthly" instead.'),
             ('--monthly', '--keep-monthly', 'Warning: "--monthly" has been deprecated. Use "--keep-monthly" instead.'),
-            ('--yearly', '--keep-yearly', 'Warning: "--yearly" has been deprecated. Use "--keep-yearly" instead.')
+            ('--yearly', '--keep-yearly', 'Warning: "--yearly" has been deprecated. Use "--keep-yearly" instead.'),
+            ('--do-not-cross-mountpoints', '--one-file-system',
+             'Warning:  "--do-no-cross-mountpoints" has been deprecated. Use "--one-file-system" instead.'),
         ]
         ]
         if args and args[0] == 'verify':
         if args and args[0] == 'verify':
             print('Warning: "borg verify" has been deprecated. Use "borg extract --dry-run" instead.')
             print('Warning: "borg verify" has been deprecated. Use "borg extract --dry-run" instead.')
@@ -552,26 +571,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                     print(warning)
                     print(warning)
         return args
         return args
 
 
-    def run(self, args=None):
-        check_extension_modules()
-        keys_dir = get_keys_dir()
-        if not os.path.exists(keys_dir):
-            os.makedirs(keys_dir)
-            os.chmod(keys_dir, stat.S_IRWXU)
-        cache_dir = get_cache_dir()
-        if not os.path.exists(cache_dir):
-            os.makedirs(cache_dir)
-            os.chmod(cache_dir, stat.S_IRWXU)
-            with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
-                fd.write(textwrap.dedent("""
-                    Signature: 8a477f597d28d172789f06886806bc55
-                    # This file is a cache directory tag created by Borg.
-                    # For information about cache directory tags, see:
-                    #       http://www.brynosaurus.com/cachedir/
-                    """).lstrip())
-        common_parser = argparse.ArgumentParser(add_help=False)
-        common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
-                                   default=False,
+    def build_parser(self, args=None, prog=None):
+        common_parser = argparse.ArgumentParser(add_help=False, prog=prog)
+        common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', default=False,
                                    help='verbose output')
                                    help='verbose output')
         common_parser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
         common_parser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
                                    help='do not load/update the file metadata cache used to detect unchanged files')
                                    help='do not load/update the file metadata cache used to detect unchanged files')
@@ -580,11 +582,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         common_parser.add_argument('--remote-path', dest='remote_path', default=RemoteRepository.remote_path, metavar='PATH',
         common_parser.add_argument('--remote-path', dest='remote_path', default=RemoteRepository.remote_path, metavar='PATH',
                                    help='set remote path to executable (default: "%(default)s")')
                                    help='set remote path to executable (default: "%(default)s")')
 
 
-        # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
-        if args:
-            args = self.preprocess_args(args)
-
-        parser = argparse.ArgumentParser(description='Borg %s - Deduplicated Backups' % __version__)
+        parser = argparse.ArgumentParser(prog=prog, description='Borg - Deduplicated Backups')
+        parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__,
+                                   help='show version number and exit')
         subparsers = parser.add_subparsers(title='Available commands')
         subparsers = parser.add_subparsers(title='Available commands')
 
 
         serve_epilog = textwrap.dedent("""
         serve_epilog = textwrap.dedent("""
@@ -699,9 +699,11 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         subparser.add_argument('-s', '--stats', dest='stats',
         subparser.add_argument('-s', '--stats', dest='stats',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='print statistics for the created archive')
                                help='print statistics for the created archive')
-        subparser.add_argument('-p', '--progress', dest='progress',
-                               action='store_true', default=False,
-                               help='print progress while creating the archive')
+        subparser.add_argument('-p', '--progress', dest='progress', const=not sys.stderr.isatty(),
+                               action='store_const', default=sys.stdin.isatty(),
+                               help="""toggle progress display while creating the archive, showing Original,
+                               Compressed and Deduplicated sizes, followed by the Number of files seen
+                               and the path being processed, default: %(default)s""")
         subparser.add_argument('-e', '--exclude', dest='excludes',
         subparser.add_argument('-e', '--exclude', dest='excludes',
                                type=ExcludePattern, action='append',
                                type=ExcludePattern, action='append',
                                metavar="PATTERN", help='exclude paths matching PATTERN')
                                metavar="PATTERN", help='exclude paths matching PATTERN')
@@ -714,9 +716,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         subparser.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval',
         subparser.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval',
                                type=int, default=300, metavar='SECONDS',
                                type=int, default=300, metavar='SECONDS',
                                help='write checkpoint every SECONDS seconds (Default: 300)')
                                help='write checkpoint every SECONDS seconds (Default: 300)')
-        subparser.add_argument('--do-not-cross-mountpoints', dest='dontcross',
+        subparser.add_argument('-x', '--one-file-system', dest='one_file_system',
                                action='store_true', default=False,
                                action='store_true', default=False,
-                               help='do not cross mount points')
+                               help='stay in same file system, do not cross mount points')
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='only store numeric user and group identifiers')
                                help='only store numeric user and group identifiers')
@@ -925,7 +927,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                                help='repository to prune')
                                help='repository to prune')
 
 
         upgrade_epilog = textwrap.dedent("""
         upgrade_epilog = textwrap.dedent("""
-        upgrade an existing Borg repository in place. this currently
+        upgrade an existing Borg repository. this currently
         only support converting an Attic repository, but may
         only support converting an Attic repository, but may
         eventually be extended to cover major Borg upgrades as well.
         eventually be extended to cover major Borg upgrades as well.
 
 
@@ -940,13 +942,6 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         the first backup after the conversion takes longer than expected
         the first backup after the conversion takes longer than expected
         due to the cache resync.
         due to the cache resync.
 
 
-        it is recommended you run this on a copy of the Attic
-        repository, in case something goes wrong, for example:
-
-            cp -a attic borg
-            borg upgrade -n borg
-            borg upgrade borg
-
         upgrade should be able to resume if interrupted, although it
         upgrade should be able to resume if interrupted, although it
         will still iterate over all segments. if you want to start
         will still iterate over all segments. if you want to start
         from scratch, use `borg delete` over the copied repository to
         from scratch, use `borg delete` over the copied repository to
@@ -954,11 +949,19 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
 
 
             borg delete borg
             borg delete borg
 
 
-        the conversion can PERMANENTLY DAMAGE YOUR REPOSITORY! Attic
-        will also NOT BE ABLE TO READ THE BORG REPOSITORY ANYMORE, as
-        the magic strings will have changed.
-
-        you have been warned.""")
+        unless ``--inplace`` is specified, the upgrade process first
+        creates a backup copy of the repository, in
+        REPOSITORY.upgrade-DATETIME, using hardlinks. this takes
+        longer than in place upgrades, but is much safer and gives
+        progress information (as opposed to ``cp -al``). once you are
+        satisfied with the conversion, you can safely destroy the
+        backup copy.
+
+        WARNING: running the upgrade in place will make the current
+        copy unusable with older version, with no way of going back
+        to previous versions. this can PERMANENTLY DAMAGE YOUR
+        REPOSITORY!  Attic CAN NOT READ BORG REPOSITORIES, as the
+        magic strings have changed. you have been warned.""")
         subparser = subparsers.add_parser('upgrade', parents=[common_parser],
         subparser = subparsers.add_parser('upgrade', parents=[common_parser],
                                           description=self.do_upgrade.__doc__,
                                           description=self.do_upgrade.__doc__,
                                           epilog=upgrade_epilog,
                                           epilog=upgrade_epilog,
@@ -967,6 +970,10 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         subparser.add_argument('-n', '--dry-run', dest='dry_run',
         subparser.add_argument('-n', '--dry-run', dest='dry_run',
                                default=False, action='store_true',
                                default=False, action='store_true',
                                help='do not change repository')
                                help='do not change repository')
+        subparser.add_argument('-i', '--inplace', dest='inplace',
+                               default=False, action='store_true',
+                               help="""rewrite repository in place, with no chance of going back to older
+                               versions of the repository.""")
         subparser.add_argument('repository', metavar='REPOSITORY', nargs='?', default='',
         subparser.add_argument('repository', metavar='REPOSITORY', nargs='?', default='',
                                type=location_validator(archive=False),
                                type=location_validator(archive=False),
                                help='path to the repository to be upgraded')
                                help='path to the repository to be upgraded')
@@ -980,9 +987,34 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices))
         subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices))
         subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?',
         subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?',
                                help='additional help on TOPIC')
                                help='additional help on TOPIC')
+        return parser
+
+    def run(self, args=None):
+        check_extension_modules()
+        keys_dir = get_keys_dir()
+        if not os.path.exists(keys_dir):
+            os.makedirs(keys_dir)
+            os.chmod(keys_dir, stat.S_IRWXU)
+        cache_dir = get_cache_dir()
+        if not os.path.exists(cache_dir):
+            os.makedirs(cache_dir)
+            os.chmod(cache_dir, stat.S_IRWXU)
+            with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
+                fd.write(textwrap.dedent("""
+                    Signature: 8a477f597d28d172789f06886806bc55
+                    # This file is a cache directory tag created by Borg.
+                    # For information about cache directory tags, see:
+                    #       http://www.brynosaurus.com/cachedir/
+                    """).lstrip())
+
+        # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
+        if args:
+            args = self.preprocess_args(args)
+        parser = self.build_parser(args)
 
 
         args = parser.parse_args(args or ['-h'])
         args = parser.parse_args(args or ['-h'])
         self.verbose = args.verbose
         self.verbose = args.verbose
+        setup_logging()
         os.umask(args.umask)
         os.umask(args.umask)
         RemoteRepository.remote_path = args.remote_path
         RemoteRepository.remote_path = args.remote_path
         RemoteRepository.umask = args.umask
         RemoteRepository.umask = args.umask
@@ -1001,7 +1033,7 @@ def sig_info_handler(signum, stack):  # pragma: no cover
                 total = loc['st'].st_size
                 total = loc['st'].st_size
             except Exception:
             except Exception:
                 pos, total = 0, 0
                 pos, total = 0, 0
-            print("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
+            logger.info("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
             break
             break
         if func in ('extract_item', ):  # extract op
         if func in ('extract_item', ):  # extract op
             path = loc['item'][b'path']
             path = loc['item'][b'path']
@@ -1009,7 +1041,7 @@ def sig_info_handler(signum, stack):  # pragma: no cover
                 pos = loc['fd'].tell()
                 pos = loc['fd'].tell()
             except Exception:
             except Exception:
                 pos = 0
                 pos = 0
-            print("{0} {1}/???".format(path, format_file_size(pos)))
+            logger.info("{0} {1}/???".format(path, format_file_size(pos)))
             break
             break
 
 
 
 
@@ -1031,22 +1063,34 @@ def main():  # pragma: no cover
     setup_signal_handlers()
     setup_signal_handlers()
     archiver = Archiver()
     archiver = Archiver()
     try:
     try:
+        msg = None
         exit_code = archiver.run(sys.argv[1:])
         exit_code = archiver.run(sys.argv[1:])
     except Error as e:
     except Error as e:
-        archiver.print_error(e.get_message() + "\n%s" % traceback.format_exc())
+        msg = e.get_message() + "\n%s" % traceback.format_exc()
         exit_code = e.exit_code
         exit_code = e.exit_code
     except RemoteRepository.RPCError as e:
     except RemoteRepository.RPCError as e:
-        archiver.print_error('Error: Remote Exception.\n%s' % str(e))
-        exit_code = 1
+        msg = 'Remote Exception.\n%s' % str(e)
+        exit_code = EXIT_ERROR
     except Exception:
     except Exception:
-        archiver.print_error('Error: Local Exception.\n%s' % traceback.format_exc())
-        exit_code = 1
+        msg = 'Local Exception.\n%s' % traceback.format_exc()
+        exit_code = EXIT_ERROR
     except KeyboardInterrupt:
     except KeyboardInterrupt:
-        archiver.print_error('Error: Keyboard interrupt.\n%s' % traceback.format_exc())
-        exit_code = 1
-    if exit_code:
-        archiver.print_error('Exiting with failure status due to previous errors')
+        msg = 'Keyboard interrupt.\n%s' % traceback.format_exc()
+        exit_code = EXIT_ERROR
+    if msg:
+        logger.error(msg)
+    exit_msg = 'terminating with %s status, rc %d'
+    if exit_code == EXIT_SUCCESS:
+        logger.info(exit_msg % ('success', exit_code))
+    elif exit_code == EXIT_WARNING:
+        logger.warning(exit_msg % ('warning', exit_code))
+    elif exit_code == EXIT_ERROR:
+        logger.error(exit_msg % ('error', exit_code))
+    else:
+        # if you see 666 in output, it usually means exit_code was None
+        logger.error(exit_msg % ('abnormal', exit_code or 666))
     sys.exit(exit_code)
     sys.exit(exit_code)
 
 
+
 if __name__ == '__main__':
 if __name__ == '__main__':
     main()
     main()

+ 34 - 10
borg/cache.py

@@ -1,7 +1,7 @@
 import configparser
 import configparser
 from .remote import cache_if_remote
 from .remote import cache_if_remote
+from collections import namedtuple
 import errno
 import errno
-import msgpack
 import os
 import os
 import stat
 import stat
 import sys
 import sys
@@ -12,11 +12,16 @@ import tarfile
 import tempfile
 import tempfile
 
 
 from .key import PlaintextKey
 from .key import PlaintextKey
+from .logger import create_logger
+logger = create_logger()
 from .helpers import Error, get_cache_dir, decode_dict, st_mtime_ns, unhexlify, int_to_bigint, \
 from .helpers import Error, get_cache_dir, decode_dict, st_mtime_ns, unhexlify, int_to_bigint, \
-    bigint_to_int
+    bigint_to_int, format_file_size, have_cython
 from .locking import UpgradableLock
 from .locking import UpgradableLock
 from .hashindex import ChunkIndex
 from .hashindex import ChunkIndex
 
 
+if have_cython():
+    import msgpack
+
 
 
 class Cache:
 class Cache:
     """Client Side cache
     """Client Side cache
@@ -47,6 +52,7 @@ class Cache:
         self.manifest = manifest
         self.manifest = manifest
         self.path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii'))
         self.path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii'))
         self.do_files = do_files
         self.do_files = do_files
+        logger.info('initializing cache')
         # Warn user before sending data to a never seen before unencrypted repository
         # Warn user before sending data to a never seen before unencrypted repository
         if not os.path.exists(self.path):
         if not os.path.exists(self.path):
             if warn_if_unencrypted and isinstance(key, PlaintextKey):
             if warn_if_unencrypted and isinstance(key, PlaintextKey):
@@ -68,16 +74,33 @@ class Cache:
             # Make sure an encrypted repository has not been swapped for an unencrypted repository
             # Make sure an encrypted repository has not been swapped for an unencrypted repository
             if self.key_type is not None and self.key_type != str(key.TYPE):
             if self.key_type is not None and self.key_type != str(key.TYPE):
                 raise self.EncryptionMethodMismatch()
                 raise self.EncryptionMethodMismatch()
+            logger.info('synchronizing cache')
             self.sync()
             self.sync()
             self.commit()
             self.commit()
 
 
     def __del__(self):
     def __del__(self):
         self.close()
         self.close()
 
 
+    def __str__(self):
+        fmt = """\
+All archives:   {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s}
+
+                       Unique chunks         Total chunks
+Chunk index:    {0.total_unique_chunks:20d} {0.total_chunks:20d}"""
+        return fmt.format(self.format_tuple())
+
+    def format_tuple(self):
+        # XXX: this should really be moved down to `hashindex.pyx`
+        Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', 'total_chunks'])
+        stats = Summary(*self.chunks.summarize())._asdict()
+        for field in ['total_size', 'total_csize', 'unique_csize']:
+            stats[field] = format_file_size(stats[field])
+        return Summary(**stats)
+
     def _confirm(self, message, env_var_override=None):
     def _confirm(self, message, env_var_override=None):
         print(message, file=sys.stderr)
         print(message, file=sys.stderr)
         if env_var_override and os.environ.get(env_var_override):
         if env_var_override and os.environ.get(env_var_override):
-            print("Yes (From {})".format(env_var_override))
+            print("Yes (From {})".format(env_var_override), file=sys.stderr)
             return True
             return True
         if not sys.stdin.isatty():
         if not sys.stdin.isatty():
             return False
             return False
@@ -146,6 +169,7 @@ class Cache:
     def _read_files(self):
     def _read_files(self):
         self.files = {}
         self.files = {}
         self._newest_mtime = 0
         self._newest_mtime = 0
+        logger.info('reading files cache')
         with open(os.path.join(self.path, 'files'), 'rb') as fd:
         with open(os.path.join(self.path, 'files'), 'rb') as fd:
             u = msgpack.Unpacker(use_list=True)
             u = msgpack.Unpacker(use_list=True)
             while True:
             while True:
@@ -267,7 +291,7 @@ class Cache:
                 unpacker.feed(data)
                 unpacker.feed(data)
                 for item in unpacker:
                 for item in unpacker:
                     if not isinstance(item, dict):
                     if not isinstance(item, dict):
-                        print('Error: Did not get expected metadata dict - archive corrupted!')
+                        logger.error('Error: Did not get expected metadata dict - archive corrupted!')
                         continue
                         continue
                     if b'chunks' in item:
                     if b'chunks' in item:
                         for chunk_id, size, csize in item[b'chunks']:
                         for chunk_id, size, csize in item[b'chunks']:
@@ -289,10 +313,10 @@ class Cache:
                     return name
                     return name
 
 
         def create_master_idx(chunk_idx):
         def create_master_idx(chunk_idx):
-            print('Synchronizing chunks cache...')
+            logger.info('Synchronizing chunks cache...')
             cached_ids = cached_archives()
             cached_ids = cached_archives()
             archive_ids = repo_archives()
             archive_ids = repo_archives()
-            print('Archives: %d, w/ cached Idx: %d, w/ outdated Idx: %d, w/o cached Idx: %d.' % (
+            logger.info('Archives: %d, w/ cached Idx: %d, w/ outdated Idx: %d, w/o cached Idx: %d.' % (
                 len(archive_ids), len(cached_ids),
                 len(archive_ids), len(cached_ids),
                 len(cached_ids - archive_ids), len(archive_ids - cached_ids), ))
                 len(cached_ids - archive_ids), len(archive_ids - cached_ids), ))
             # deallocates old hashindex, creates empty hashindex:
             # deallocates old hashindex, creates empty hashindex:
@@ -304,12 +328,12 @@ class Cache:
                     archive_name = lookup_name(archive_id)
                     archive_name = lookup_name(archive_id)
                     if archive_id in cached_ids:
                     if archive_id in cached_ids:
                         archive_chunk_idx_path = mkpath(archive_id)
                         archive_chunk_idx_path = mkpath(archive_id)
-                        print("Reading cached archive chunk index for %s ..." % archive_name)
+                        logger.info("Reading cached archive chunk index for %s ..." % archive_name)
                         archive_chunk_idx = ChunkIndex.read(archive_chunk_idx_path)
                         archive_chunk_idx = ChunkIndex.read(archive_chunk_idx_path)
                     else:
                     else:
-                        print('Fetching and building archive index for %s ...' % archive_name)
+                        logger.info('Fetching and building archive index for %s ...' % archive_name)
                         archive_chunk_idx = fetch_and_build_idx(archive_id, repository, self.key)
                         archive_chunk_idx = fetch_and_build_idx(archive_id, repository, self.key)
-                    print("Merging into master chunks index ...")
+                    logger.info("Merging into master chunks index ...")
                     if chunk_idx is None:
                     if chunk_idx is None:
                         # we just use the first archive's idx as starting point,
                         # we just use the first archive's idx as starting point,
                         # to avoid growing the hash table from 0 size and also
                         # to avoid growing the hash table from 0 size and also
@@ -317,7 +341,7 @@ class Cache:
                         chunk_idx = archive_chunk_idx
                         chunk_idx = archive_chunk_idx
                     else:
                     else:
                         chunk_idx.merge(archive_chunk_idx)
                         chunk_idx.merge(archive_chunk_idx)
-            print('Done.')
+            logger.info('Done.')
             return chunk_idx
             return chunk_idx
 
 
         def legacy_cleanup():
         def legacy_cleanup():

+ 11 - 8
borg/fuse.py

@@ -2,17 +2,19 @@ from collections import defaultdict
 import errno
 import errno
 import io
 import io
 import llfuse
 import llfuse
-import msgpack
 import os
 import os
 import stat
 import stat
 import tempfile
 import tempfile
 import time
 import time
 from .archive import Archive
 from .archive import Archive
-from .helpers import daemonize
+from .helpers import daemonize, have_cython
 from .remote import cache_if_remote
 from .remote import cache_if_remote
 
 
+if have_cython():
+    import msgpack
+
 # Does this version of llfuse support ns precision?
 # Does this version of llfuse support ns precision?
-have_fuse_mtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns')
+have_fuse_xtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns')
 
 
 
 
 class ItemCache:
 class ItemCache:
@@ -153,14 +155,15 @@ class FuseOperations(llfuse.Operations):
         entry.st_size = size
         entry.st_size = size
         entry.st_blksize = 512
         entry.st_blksize = 512
         entry.st_blocks = dsize / 512
         entry.st_blocks = dsize / 512
-        if have_fuse_mtime_ns:
-            entry.st_atime_ns = item[b'mtime']
+        # note: older archives only have mtime (not atime nor ctime)
+        if have_fuse_xtime_ns:
+            entry.st_atime_ns = item.get(b'atime') or item[b'mtime']
             entry.st_mtime_ns = item[b'mtime']
             entry.st_mtime_ns = item[b'mtime']
-            entry.st_ctime_ns = item[b'mtime']
+            entry.st_ctime_ns = item.get(b'ctime') or item[b'mtime']
         else:
         else:
-            entry.st_atime = item[b'mtime'] / 1e9
+            entry.st_atime = (item.get(b'atime') or item[b'mtime']) / 1e9
             entry.st_mtime = item[b'mtime'] / 1e9
             entry.st_mtime = item[b'mtime'] / 1e9
-            entry.st_ctime = item[b'mtime'] / 1e9
+            entry.st_ctime = (item.get(b'ctime') or item[b'mtime']) / 1e9
         return entry
         return entry
 
 
     def listxattr(self, inode):
     def listxattr(self, inode):

+ 122 - 45
borg/helpers.py

@@ -1,5 +1,4 @@
-from .support import argparse  # see support/__init__.py docstring
-                               # DEPRECATED - remove after requiring py 3.4
+from .support import argparse  # see support/__init__.py docstring, DEPRECATED - remove after requiring py 3.4
 
 
 import binascii
 import binascii
 from collections import namedtuple
 from collections import namedtuple
@@ -9,6 +8,12 @@ import os
 import pwd
 import pwd
 import queue
 import queue
 import re
 import re
+try:
+    from shutil import get_terminal_size
+except ImportError:
+    def get_terminal_size(fallback=(80, 24)):
+        TerminalSize = namedtuple('TerminalSize', ['columns', 'lines'])
+        return TerminalSize(int(os.environ.get('COLUMNS', fallback[0])), int(os.environ.get('LINES', fallback[1])))
 import sys
 import sys
 import time
 import time
 import unicodedata
 import unicodedata
@@ -17,11 +22,34 @@ from datetime import datetime, timezone, timedelta
 from fnmatch import translate
 from fnmatch import translate
 from operator import attrgetter
 from operator import attrgetter
 
 
-import msgpack
 
 
-from . import hashindex
-from . import chunker
-from . import crypto
+def have_cython():
+    """allow for a way to disable Cython includes
+
+    this is used during usage docs build, in setup.py. It is to avoid
+    loading the Cython libraries which are built, but sometimes not in
+    the search path (namely, during Tox runs).
+
+    we simply check an environment variable (``BORG_CYTHON_DISABLE``)
+    which, when set (to anything) will disable includes of Cython
+    libraries in key places to enable usage docs to be built.
+
+    :returns: True if Cython is available, False otherwise.
+    """
+    return not os.environ.get('BORG_CYTHON_DISABLE')
+
+if have_cython():
+    from . import hashindex
+    from . import chunker
+    from . import crypto
+    import msgpack
+
+
+# return codes returned by borg command
+# when borg is killed by signal N, rc = 128 + N
+EXIT_SUCCESS = 0  # everything done, no problems
+EXIT_WARNING = 1  # reached normal end of operation, but there were issues
+EXIT_ERROR = 2  # terminated abruptly, did not reach end of operation
 
 
 QUEUE_DEBUG = False
 QUEUE_DEBUG = False
 
 
@@ -29,10 +57,17 @@ QUEUE_DEBUG = False
 class Error(Exception):
 class Error(Exception):
     """Error base class"""
     """Error base class"""
 
 
-    exit_code = 1
+    # if we raise such an Error and it is only catched by the uppermost
+    # exception handler (that exits short after with the given exit_code),
+    # it is always a (fatal and abrupt) EXIT_ERROR, never just a warning.
+    exit_code = EXIT_ERROR
 
 
     def get_message(self):
     def get_message(self):
-        return 'Error: ' + type(self).__doc__.format(*self.args)
+        return type(self).__doc__.format(*self.args)
+
+
+class IntegrityError(Error):
+    """Data integrity error"""
 
 
 
 
 class ExtensionModuleError(Error):
 class ExtensionModuleError(Error):
@@ -144,27 +179,41 @@ class Statistics:
         if unique:
         if unique:
             self.usize += csize
             self.usize += csize
 
 
-    def print_(self, label, cache):
-        total_size, total_csize, unique_size, unique_csize, total_unique_chunks, total_chunks = cache.chunks.summarize()
-        print()
-        print('                       Original size      Compressed size    Deduplicated size')
-        print('%-15s %20s %20s %20s' % (label, format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize)))
-        print('All archives:   %20s %20s %20s' % (format_file_size(total_size), format_file_size(total_csize), format_file_size(unique_csize)))
-        print()
-        print('                       Unique chunks         Total chunks')
-        print('Chunk index:    %20d %20d' % (total_unique_chunks, total_chunks))
-
-    def show_progress(self, item=None, final=False):
+    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):
+        columns, lines = get_terminal_size()
         if not final:
         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 ''
             path = remove_surrogates(item[b'path']) if item else ''
-            if len(path) > 43:
-                path = '%s...%s' % (path[:20], path[-20:])
-            msg = '%9s O %9s C %9s D %-43s' % (
-                format_file_size(self.osize), format_file_size(self.csize), format_file_size(self.usize), path)
+            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:
         else:
-            msg = ' ' * 79
-        print(msg, end='\r')
-        sys.stdout.flush()
+            msg = ' ' * columns
+        print(msg, file=stream or sys.stderr, end="\r")
+        (stream or sys.stderr).flush()
 
 
 
 
 def get_keys_dir():
 def get_keys_dir():
@@ -230,7 +279,7 @@ def exclude_path(path, patterns):
 
 
 def normalized(func):
 def normalized(func):
     """ Decorator for the Pattern match methods, returning a wrapper that
     """ Decorator for the Pattern match methods, returning a wrapper that
-    normalizes OSX paths to match the normalized pattern on OSX, and 
+    normalizes OSX paths to match the normalized pattern on OSX, and
     returning the original method on other platforms"""
     returning the original method on other platforms"""
     @wraps(func)
     @wraps(func)
     def normalize_wrapper(self, path):
     def normalize_wrapper(self, path):
@@ -426,27 +475,33 @@ def format_file_mode(mod):
     return '%s%s%s' % (x(mod // 64), x(mod // 8), x(mod))
     return '%s%s%s' % (x(mod // 64), x(mod // 8), x(mod))
 
 
 
 
-def format_file_size(v):
+def format_file_size(v, precision=2):
     """Format file size into a human friendly format
     """Format file size into a human friendly format
     """
     """
-    if abs(v) > 10**12:
-        return '%.2f TB' % (v / 10**12)
-    elif abs(v) > 10**9:
-        return '%.2f GB' % (v / 10**9)
-    elif abs(v) > 10**6:
-        return '%.2f MB' % (v / 10**6)
-    elif abs(v) > 10**3:
-        return '%.2f kB' % (v / 10**3)
-    else:
-        return '%d B' % v
+    return sizeof_fmt_decimal(v, suffix='B', sep=' ', precision=precision)
 
 
 
 
-def format_archive(archive):
-    return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
+def sizeof_fmt(num, suffix='B', units=None, power=None, sep='', precision=2):
+    for unit in units[:-1]:
+        if abs(round(num, precision)) < power:
+            if isinstance(num, int):
+                return "{}{}{}{}".format(num, sep, unit, suffix)
+            else:
+                return "{:3.{}f}{}{}{}".format(num, precision, sep, unit, suffix)
+        num /= float(power)
+    return "{:.{}f}{}{}{}".format(num, precision, sep, units[-1], suffix)
 
 
 
 
-class IntegrityError(Error):
-    """Data integrity error"""
+def sizeof_fmt_iec(num, suffix='B', sep='', precision=2):
+    return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'], power=1024)
+
+
+def sizeof_fmt_decimal(num, suffix='B', sep='', precision=2):
+    return sizeof_fmt(num, suffix=suffix, sep=sep, precision=precision, units=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], power=1000)
+
+
+def format_archive(archive):
+    return '%-36s %s' % (archive.name, to_localtime(archive.ts).strftime('%c'))
 
 
 
 
 def memoize(function):
 def memoize(function):
@@ -498,14 +553,24 @@ def posix_acl_use_stored_uid_gid(acl):
     """Replace the user/group field with the stored uid/gid
     """Replace the user/group field with the stored uid/gid
     """
     """
     entries = []
     entries = []
-    for entry in acl.decode('ascii').split('\n'):
+    for entry in safe_decode(acl).split('\n'):
         if entry:
         if entry:
             fields = entry.split(':')
             fields = entry.split(':')
             if len(fields) == 4:
             if len(fields) == 4:
                 entries.append(':'.join([fields[0], fields[3], fields[2]]))
                 entries.append(':'.join([fields[0], fields[3], fields[2]]))
             else:
             else:
                 entries.append(entry)
                 entries.append(entry)
-    return ('\n'.join(entries)).encode('ascii')
+    return safe_encode('\n'.join(entries))
+
+
+def safe_decode(s, coding='utf-8', errors='surrogateescape'):
+    """decode bytes to str, with round-tripping "invalid" bytes"""
+    return s.decode(coding, errors)
+
+
+def safe_encode(s, coding='utf-8', errors='surrogateescape'):
+    """encode str to bytes, with round-tripping "invalid" bytes"""
+    return s.encode(coding, errors)
 
 
 
 
 class Location:
 class Location:
@@ -685,7 +750,13 @@ class StableDict(dict):
 
 
 
 
 if sys.version < '3.3':
 if sys.version < '3.3':
-    # st_mtime_ns attribute only available in 3.3+
+    # st_xtime_ns attributes only available in 3.3+
+    def st_atime_ns(st):
+        return int(st.st_atime * 1e9)
+
+    def st_ctime_ns(st):
+        return int(st.st_ctime * 1e9)
+
     def st_mtime_ns(st):
     def st_mtime_ns(st):
         return int(st.st_mtime * 1e9)
         return int(st.st_mtime * 1e9)
 
 
@@ -695,6 +766,12 @@ if sys.version < '3.3':
             data = data.encode('ascii')
             data = data.encode('ascii')
         return binascii.unhexlify(data)
         return binascii.unhexlify(data)
 else:
 else:
+    def st_atime_ns(st):
+        return st.st_atime_ns
+
+    def st_ctime_ns(st):
+        return st.st_ctime_ns
+
     def st_mtime_ns(st):
     def st_mtime_ns(st):
         return st.st_mtime_ns
         return st.st_mtime_ns
 
 

+ 17 - 13
borg/key.py

@@ -2,14 +2,18 @@ from binascii import hexlify, a2b_base64, b2a_base64
 import configparser
 import configparser
 import getpass
 import getpass
 import os
 import os
-import msgpack
 import textwrap
 import textwrap
 import hmac
 import hmac
 from hashlib import sha256
 from hashlib import sha256
 
 
-from .crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
-from .compress import Compressor, COMPR_BUFFER
-from .helpers import IntegrityError, get_keys_dir, Error
+from .helpers import IntegrityError, get_keys_dir, Error, have_cython
+from .logger import create_logger
+logger = create_logger()
+
+if have_cython():
+    from .crypto import pbkdf2_sha256, get_random_bytes, AES, bytes_to_long, long_to_bytes, bytes_to_int, num_aes_blocks
+    from .compress import Compressor, COMPR_BUFFER
+    import msgpack
 
 
 PREFIX = b'\0' * 8
 PREFIX = b'\0' * 8
 
 
@@ -88,7 +92,7 @@ class PlaintextKey(KeyBase):
 
 
     @classmethod
     @classmethod
     def create(cls, repository, args):
     def create(cls, repository, args):
-        print('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile|passphrase" to enable encryption.')
+        logger.info('Encryption NOT enabled.\nUse the "--encryption=repokey|keyfile|passphrase" to enable encryption.')
         return cls(repository)
         return cls(repository)
 
 
     @classmethod
     @classmethod
@@ -190,12 +194,12 @@ class Passphrase(str):
             if allow_empty or passphrase:
             if allow_empty or passphrase:
                 passphrase2 = cls.getpass('Enter same passphrase again: ')
                 passphrase2 = cls.getpass('Enter same passphrase again: ')
                 if passphrase == passphrase2:
                 if passphrase == passphrase2:
-                    print('Remember your passphrase. Your data will be inaccessible without it.')
+                    logger.info('Remember your passphrase. Your data will be inaccessible without it.')
                     return passphrase
                     return passphrase
                 else:
                 else:
-                    print('Passphrases do not match')
+                    print('Passphrases do not match', file=sys.stderr)
             else:
             else:
-                print('Passphrase must not be blank')
+                print('Passphrase must not be blank', file=sys.stderr)
 
 
     def __repr__(self):
     def __repr__(self):
         return '<Passphrase "***hidden***">'
         return '<Passphrase "***hidden***">'
@@ -215,8 +219,8 @@ class PassphraseKey(AESKeyBase):
     @classmethod
     @classmethod
     def create(cls, repository, args):
     def create(cls, repository, args):
         key = cls(repository)
         key = cls(repository)
-        print('WARNING: "passphrase" mode is deprecated and will be removed in 1.0.')
-        print('If you want something similar (but with less issues), use "repokey" mode.')
+        logger.warning('WARNING: "passphrase" mode is deprecated and will be removed in 1.0.')
+        logger.warning('If you want something similar (but with less issues), use "repokey" mode.')
         passphrase = Passphrase.new(allow_empty=False)
         passphrase = Passphrase.new(allow_empty=False)
         key.init(repository, passphrase)
         key.init(repository, passphrase)
         return key
         return key
@@ -324,7 +328,7 @@ class KeyfileKeyBase(AESKeyBase):
     def change_passphrase(self):
     def change_passphrase(self):
         passphrase = Passphrase.new(allow_empty=True)
         passphrase = Passphrase.new(allow_empty=True)
         self.save(self.target, passphrase)
         self.save(self.target, passphrase)
-        print('Key updated')
+        logger.info('Key updated')
 
 
     @classmethod
     @classmethod
     def create(cls, repository, args):
     def create(cls, repository, args):
@@ -335,8 +339,8 @@ class KeyfileKeyBase(AESKeyBase):
         key.init_ciphers()
         key.init_ciphers()
         target = key.get_new_target(args)
         target = key.get_new_target(args)
         key.save(target, passphrase)
         key.save(target, passphrase)
-        print('Key in "%s" created.' % target)
-        print('Keep this key safe. Your data will be inaccessible without it.')
+        logger.info('Key in "%s" created.' % target)
+        logger.info('Keep this key safe. Your data will be inaccessible without it.')
         return key
         return key
 
 
     def save(self, target, passphrase):
     def save(self, target, passphrase):

+ 8 - 5
borg/locking.py

@@ -2,7 +2,6 @@ import errno
 import json
 import json
 import os
 import os
 import socket
 import socket
-import threading
 import time
 import time
 
 
 from borg.helpers import Error
 from borg.helpers import Error
@@ -10,13 +9,17 @@ from borg.helpers import Error
 ADD, REMOVE = 'add', 'remove'
 ADD, REMOVE = 'add', 'remove'
 SHARED, EXCLUSIVE = 'shared', 'exclusive'
 SHARED, EXCLUSIVE = 'shared', 'exclusive'
 
 
+# only determine the PID and hostname once.
+# for FUSE mounts, we fork a child process that needs to release
+# the lock made by the parent, so it needs to use the same PID for that.
+_pid = os.getpid()
+_hostname = socket.gethostname()
+
 
 
 def get_id():
 def get_id():
     """Get identification tuple for 'us'"""
     """Get identification tuple for 'us'"""
-    hostname = socket.gethostname()
-    pid = os.getpid()
-    tid = threading.current_thread().ident & 0xffffffff
-    return hostname, pid, tid
+    thread_id = 0
+    return _hostname, _pid, thread_id
 
 
 
 
 class TimeoutTimer:
 class TimeoutTimer:

+ 85 - 0
borg/logger.py

@@ -0,0 +1,85 @@
+"""logging facilities
+
+The way to use this is as follows:
+
+* each module declares its own logger, using:
+
+    from .logger import create_logger
+    logger = create_logger()
+
+* then each module uses logger.info/warning/debug/etc according to the
+  level it believes is appropriate:
+
+    logger.debug('debugging info for developers or power users')
+    logger.info('normal, informational output')
+    logger.warning('warn about a non-fatal error or sth else')
+    logger.error('a fatal error')
+
+  ... and so on. see the `logging documentation
+  <https://docs.python.org/3/howto/logging.html#when-to-use-logging>`_
+  for more information
+
+* console interaction happens on stderr, that includes interactive
+  reporting functions like `help`, `info` and `list`
+
+* ...except ``input()`` is special, because we can't control the
+  stream it is using, unfortunately. we assume that it won't clutter
+  stdout, because interaction would be broken then anyways
+
+* what is output on INFO level is additionally controlled by commandline
+  flags
+"""
+
+import inspect
+import logging
+import sys
+
+
+def setup_logging(stream=None):
+    """setup logging module according to the arguments provided
+
+    this sets up a stream handler logger on stderr (by default, if no
+    stream is provided).
+    """
+    logging.raiseExceptions = False
+    l = logging.getLogger('')
+    sh = logging.StreamHandler(stream)
+    # other formatters will probably want this, but let's remove
+    # clutter on stderr
+    # example:
+    # sh.setFormatter(logging.Formatter('%(name)s: %(message)s'))
+    l.addHandler(sh)
+    l.setLevel(logging.INFO)
+    return sh
+
+
+def find_parent_module():
+    """find the name of a the first module calling this module
+
+    if we cannot find it, we return the current module's name
+    (__name__) instead.
+    """
+    try:
+        frame = inspect.currentframe().f_back
+        module = inspect.getmodule(frame)
+        while module is None or module.__name__ == __name__:
+            frame = frame.f_back
+            module = inspect.getmodule(frame)
+        return module.__name__
+    except AttributeError:
+        # somehow we failed to find our module
+        # return the logger module name by default
+        return __name__
+
+
+def create_logger(name=None):
+    """create a Logger object with the proper path, which is returned by
+    find_parent_module() by default, or is provided via the commandline
+
+    this is really a shortcut for:
+
+        logger = logging.getLogger(__name__)
+
+    we use it to avoid errors and provide a more standard API.
+    """
+    return logging.getLogger(name or find_parent_module())

+ 9 - 9
borg/platform_darwin.pyx

@@ -1,5 +1,5 @@
 import os
 import os
-from .helpers import user2uid, group2gid
+from .helpers import user2uid, group2gid, safe_decode, safe_encode
 
 
 API_VERSION = 2
 API_VERSION = 2
 
 
@@ -20,7 +20,7 @@ def _remove_numeric_id_if_possible(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
     """
     """
     entries = []
     entries = []
-    for entry in acl.decode('ascii').split('\n'):
+    for entry in safe_decode(acl).split('\n'):
         if entry:
         if entry:
             fields = entry.split(':')
             fields = entry.split(':')
             if fields[0] == 'user':
             if fields[0] == 'user':
@@ -30,22 +30,22 @@ def _remove_numeric_id_if_possible(acl):
                 if group2gid(fields[2]) is not None:
                 if group2gid(fields[2]) is not None:
                     fields[1] = fields[3] = ''
                     fields[1] = fields[3] = ''
             entries.append(':'.join(fields))
             entries.append(':'.join(fields))
-    return ('\n'.join(entries)).encode('ascii')
+    return safe_encode('\n'.join(entries))
 
 
 
 
 def _remove_non_numeric_identifier(acl):
 def _remove_non_numeric_identifier(acl):
     """Remove user and group names from the acl
     """Remove user and group names from the acl
     """
     """
     entries = []
     entries = []
-    for entry in acl.split(b'\n'):
+    for entry in safe_decode(acl).split('\n'):
         if entry:
         if entry:
-            fields = entry.split(b':')
-            if fields[0] in (b'user', b'group'):
-                fields[2] = b''
-                entries.append(b':'.join(fields))
+            fields = entry.split(':')
+            if fields[0] in ('user', 'group'):
+                fields[2] = ''
+                entries.append(':'.join(fields))
             else:
             else:
                 entries.append(entry)
                 entries.append(entry)
-    return b'\n'.join(entries)
+    return safe_encode('\n'.join(entries))
 
 
 
 
 def acl_get(path, item, st, numeric_owner=False):
 def acl_get(path, item, st, numeric_owner=False):

+ 3 - 3
borg/platform_freebsd.pyx

@@ -1,5 +1,5 @@
 import os
 import os
-from .helpers import posix_acl_use_stored_uid_gid
+from .helpers import posix_acl_use_stored_uid_gid, safe_encode, safe_decode
 
 
 API_VERSION = 2
 API_VERSION = 2
 
 
@@ -78,14 +78,14 @@ cdef _nfs4_use_stored_uid_gid(acl):
     """Replace the user/group field with the stored uid/gid
     """Replace the user/group field with the stored uid/gid
     """
     """
     entries = []
     entries = []
-    for entry in acl.decode('ascii').split('\n'):
+    for entry in safe_decode(acl).split('\n'):
         if entry:
         if entry:
             if entry.startswith('user:') or entry.startswith('group:'):
             if entry.startswith('user:') or entry.startswith('group:'):
                 fields = entry.split(':')
                 fields = entry.split(':')
                 entries.append(':'.join(fields[0], fields[5], *fields[2:-1]))
                 entries.append(':'.join(fields[0], fields[5], *fields[2:-1]))
             else:
             else:
                 entries.append(entry)
                 entries.append(entry)
-    return ('\n'.join(entries)).encode('ascii')
+    return safe_encode('\n'.join(entries))
 
 
 
 
 def acl_set(path, item, numeric_owner=False):
 def acl_set(path, item, numeric_owner=False):

+ 10 - 10
borg/platform_linux.pyx

@@ -1,7 +1,7 @@
 import os
 import os
 import re
 import re
 from stat import S_ISLNK
 from stat import S_ISLNK
-from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid
+from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode
 
 
 API_VERSION = 2
 API_VERSION = 2
 
 
@@ -31,22 +31,22 @@ 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
     """
     """
     entries = []
     entries = []
-    for entry in acl.decode('ascii').split('\n'):
+    for entry in safe_decode(acl).split('\n'):
         if entry:
         if entry:
             fields = entry.split(':')
             fields = entry.split(':')
             if fields[0] == 'user' and fields[1]:
             if fields[0] == 'user' and fields[1]:
-                fields[1] = user2uid(fields[1], fields[3])
+                fields[1] = str(user2uid(fields[1], fields[3]))
             elif fields[0] == 'group' and fields[1]:
             elif fields[0] == 'group' and fields[1]:
-                fields[1] = group2gid(fields[1], fields[3])
-            entries.append(':'.join(entry.split(':')[:3]))
-    return ('\n'.join(entries)).encode('ascii')
+                fields[1] = str(group2gid(fields[1], fields[3]))
+            entries.append(':'.join(fields[:3]))
+    return safe_encode('\n'.join(entries))
 
 
 
 
 cdef acl_append_numeric_ids(acl):
 cdef acl_append_numeric_ids(acl):
     """Extend the "POSIX 1003.1e draft standard 17" format with an additional uid/gid field
     """Extend the "POSIX 1003.1e draft standard 17" format with an additional uid/gid field
     """
     """
     entries = []
     entries = []
-    for entry in _comment_re.sub('', acl.decode('ascii')).split('\n'):
+    for entry in _comment_re.sub('', safe_decode(acl)).split('\n'):
         if entry:
         if entry:
             type, name, permission = entry.split(':')
             type, name, permission = entry.split(':')
             if name and type == 'user':
             if name and type == 'user':
@@ -55,14 +55,14 @@ cdef acl_append_numeric_ids(acl):
                 entries.append(':'.join([type, name, permission, str(group2gid(name, name))]))
                 entries.append(':'.join([type, name, permission, str(group2gid(name, name))]))
             else:
             else:
                 entries.append(entry)
                 entries.append(entry)
-    return ('\n'.join(entries)).encode('ascii')
+    return safe_encode('\n'.join(entries))
 
 
 
 
 cdef acl_numeric_ids(acl):
 cdef acl_numeric_ids(acl):
     """Replace the "POSIX 1003.1e draft standard 17" user/group field with uid/gid
     """Replace the "POSIX 1003.1e draft standard 17" user/group field with uid/gid
     """
     """
     entries = []
     entries = []
-    for entry in _comment_re.sub('', acl.decode('ascii')).split('\n'):
+    for entry in _comment_re.sub('', safe_decode(acl)).split('\n'):
         if entry:
         if entry:
             type, name, permission = entry.split(':')
             type, name, permission = entry.split(':')
             if name and type == 'user':
             if name and type == 'user':
@@ -73,7 +73,7 @@ cdef acl_numeric_ids(acl):
                 entries.append(':'.join([type, gid, permission, gid]))
                 entries.append(':'.join([type, gid, permission, gid]))
             else:
             else:
                 entries.append(entry)
                 entries.append(entry)
-    return ('\n'.join(entries)).encode('ascii')
+    return safe_encode('\n'.join(entries))
 
 
 
 
 def acl_get(path, item, st, numeric_owner=False):
 def acl_get(path, item, st, numeric_owner=False):

+ 4 - 2
borg/remote.py

@@ -1,6 +1,5 @@
 import errno
 import errno
 import fcntl
 import fcntl
-import msgpack
 import os
 import os
 import select
 import select
 import shlex
 import shlex
@@ -11,9 +10,12 @@ import traceback
 
 
 from . import __version__
 from . import __version__
 
 
-from .helpers import Error, IntegrityError
+from .helpers import Error, IntegrityError, have_cython
 from .repository import Repository
 from .repository import Repository
 
 
+if have_cython():
+    import msgpack
+
 BUFSIZE = 10 * 1024 * 1024
 BUFSIZE = 10 * 1024 * 1024
 
 
 
 

+ 8 - 5
borg/repository.py

@@ -2,14 +2,18 @@ from configparser import RawConfigParser
 from binascii import hexlify
 from binascii import hexlify
 from itertools import islice
 from itertools import islice
 import errno
 import errno
+import logging
+logger = logging.getLogger(__name__)
+
 import os
 import os
 import shutil
 import shutil
 import struct
 import struct
 import sys
 import sys
 from zlib import crc32
 from zlib import crc32
 
 
-from .hashindex import NSIndex
-from .helpers import Error, IntegrityError, read_msgpack, write_msgpack, unhexlify
+from .helpers import Error, IntegrityError, read_msgpack, write_msgpack, unhexlify, have_cython
+if have_cython():
+    from .hashindex import NSIndex
 from .locking import UpgradableLock
 from .locking import UpgradableLock
 from .lrucache import LRUCache
 from .lrucache import LRUCache
 
 
@@ -278,7 +282,7 @@ class Repository:
         def report_error(msg):
         def report_error(msg):
             nonlocal error_found
             nonlocal error_found
             error_found = True
             error_found = True
-            print(msg, file=sys.stderr)
+            logger.error(msg)
 
 
         assert not self._active_txn
         assert not self._active_txn
         try:
         try:
@@ -546,11 +550,10 @@ class LoggedIO:
     def recover_segment(self, segment, filename):
     def recover_segment(self, segment, filename):
         if segment in self.fds:
         if segment in self.fds:
             del self.fds[segment]
             del self.fds[segment]
-        # FIXME: save a copy of the original file
         with open(filename, 'rb') as fd:
         with open(filename, 'rb') as fd:
             data = memoryview(fd.read())
             data = memoryview(fd.read())
         os.rename(filename, filename + '.beforerecover')
         os.rename(filename, filename + '.beforerecover')
-        print('attempting to recover ' + filename, file=sys.stderr)
+        logger.info('attempting to recover ' + filename)
         with open(filename, 'wb') as fd:
         with open(filename, 'wb') as fd:
             fd.write(MAGIC)
             fd.write(MAGIC)
             while len(data) >= self.header_fmt.size:
             while len(data) >= self.header_fmt.size:

+ 3 - 2
borg/testsuite/__init__.py

@@ -85,8 +85,9 @@ class BaseTestCase(unittest.TestCase):
                 if fuse and not have_fuse_mtime_ns:
                 if fuse and not have_fuse_mtime_ns:
                     d1.append(round(st_mtime_ns(s1), -4))
                     d1.append(round(st_mtime_ns(s1), -4))
                     d2.append(round(st_mtime_ns(s2), -4))
                     d2.append(round(st_mtime_ns(s2), -4))
-                d1.append(round(st_mtime_ns(s1), st_mtime_ns_round))
-                d2.append(round(st_mtime_ns(s2), st_mtime_ns_round))
+                else:
+                    d1.append(round(st_mtime_ns(s1), st_mtime_ns_round))
+                    d2.append(round(st_mtime_ns(s2), st_mtime_ns_round))
             d1.append(get_all(path1, follow_symlinks=False))
             d1.append(get_all(path1, follow_symlinks=False))
             d2.append(get_all(path2, follow_symlinks=False))
             d2.append(get_all(path2, follow_symlinks=False))
             self.assert_equal(d1, d2)
             self.assert_equal(d1, d2)

+ 146 - 38
borg/testsuite/archiver.py

@@ -1,5 +1,6 @@
 from binascii import hexlify
 from binascii import hexlify
 from configparser import RawConfigParser
 from configparser import RawConfigParser
+import errno
 import os
 import os
 from io import StringIO
 from io import StringIO
 import stat
 import stat
@@ -19,7 +20,7 @@ from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
 from ..archiver import Archiver
 from ..archiver import Archiver
 from ..cache import Cache
 from ..cache import Cache
 from ..crypto import bytes_to_long, num_aes_blocks
 from ..crypto import bytes_to_long, num_aes_blocks
-from ..helpers import Manifest
+from ..helpers import Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, st_atime_ns, st_mtime_ns
 from ..remote import RemoteRepository, PathNotAllowed
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
 from ..repository import Repository
 from . import BaseTestCase
 from . import BaseTestCase
@@ -70,14 +71,83 @@ class environment_variable:
             else:
             else:
                 os.environ[k] = v
                 os.environ[k] = v
 
 
+def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
+    if fork:
+        try:
+            if exe is None:
+                borg = (sys.executable, '-m', 'borg.archiver')
+            elif isinstance(exe, str):
+                borg = (exe, )
+            elif not isinstance(exe, tuple):
+                raise ValueError('exe must be None, a tuple or a str')
+            output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
+            ret = 0
+        except subprocess.CalledProcessError as e:
+            output = e.output
+            ret = e.returncode
+        return ret, os.fsdecode(output)
+    else:
+        stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
+        try:
+            sys.stdin = StringIO()
+            sys.stdout = sys.stderr = output = StringIO()
+            if archiver is None:
+                archiver = Archiver()
+            ret = archiver.run(list(args))
+            return ret, output.getvalue()
+        finally:
+            sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
 
 
-class ArchiverTestCaseBase(BaseTestCase):
 
 
+# check if the binary "borg.exe" is available
+try:
+    exec_cmd('help', exe='borg.exe', fork=True)
+    BORG_EXES = ['python', 'binary', ]
+except (IOError, OSError) as err:
+    if err.errno != errno.ENOENT:
+        raise
+    BORG_EXES = ['python', ]
+
+
+@pytest.fixture(params=BORG_EXES)
+def cmd(request):
+    if request.param == 'python':
+        exe = None
+    elif request.param == 'binary':
+        exe = 'borg.exe'
+    else:
+        raise ValueError("param must be 'python' or 'binary'")
+    def exec_fn(*args, **kw):
+        return exec_cmd(*args, exe=exe, fork=True, **kw)
+    return exec_fn
+
+
+def test_return_codes(cmd, tmpdir):
+    repo = tmpdir.mkdir('repo')
+    input = tmpdir.mkdir('input')
+    output = tmpdir.mkdir('output')
+    input.join('test_file').write('content')
+    rc, out = cmd('init', '%s' % str(repo))
+    assert rc == EXIT_SUCCESS
+    rc, out = cmd('create', '%s::archive' % repo, str(input))
+    assert rc == EXIT_SUCCESS
+    with changedir(str(output)):
+        rc, out = cmd('extract', '%s::archive' % repo)
+        assert rc == EXIT_SUCCESS
+    rc, out = cmd('extract', '%s::archive' % repo, 'does/not/match')
+    assert rc == EXIT_WARNING  # pattern did not match
+    rc, out = cmd('create', '%s::archive' % repo, str(input))
+    assert rc == EXIT_ERROR  # duplicate archive name
+
+
+class ArchiverTestCaseBase(BaseTestCase):
+    EXE = None  # python source based
+    FORK_DEFAULT = False
     prefix = ''
     prefix = ''
 
 
     def setUp(self):
     def setUp(self):
         os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
         os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
-        self.archiver = Archiver()
+        self.archiver = not self.FORK_DEFAULT and Archiver() or None
         self.tmpdir = tempfile.mkdtemp()
         self.tmpdir = tempfile.mkdtemp()
         self.repository_path = os.path.join(self.tmpdir, 'repository')
         self.repository_path = os.path.join(self.tmpdir, 'repository')
         self.repository_location = self.prefix + self.repository_path
         self.repository_location = self.prefix + self.repository_path
@@ -102,34 +172,15 @@ class ArchiverTestCaseBase(BaseTestCase):
         shutil.rmtree(self.tmpdir)
         shutil.rmtree(self.tmpdir)
 
 
     def cmd(self, *args, **kw):
     def cmd(self, *args, **kw):
-        exit_code = kw.get('exit_code', 0)
-        fork = kw.get('fork', False)
-        if fork:
-            try:
-                output = subprocess.check_output((sys.executable, '-m', 'borg.archiver') + args)
-                ret = 0
-            except subprocess.CalledProcessError as e:
-                output = e.output
-                ret = e.returncode
-            output = os.fsdecode(output)
-            if ret != exit_code:
-                print(output)
-            self.assert_equal(exit_code, ret)
-            return output
-        args = list(args)
-        stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
-        try:
-            sys.stdin = StringIO()
-            output = StringIO()
-            sys.stdout = sys.stderr = output
-            ret = self.archiver.run(args)
-            sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
-            if ret != exit_code:
-                print(output.getvalue())
-            self.assert_equal(exit_code, ret)
-            return output.getvalue()
-        finally:
-            sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
+        exit_code = kw.pop('exit_code', 0)
+        fork = kw.pop('fork', None)
+        if fork is None:
+            fork = self.FORK_DEFAULT
+        ret, output = exec_cmd(*args, fork=fork, exe=self.EXE, archiver=self.archiver, **kw)
+        if ret != exit_code:
+            print(output)
+        self.assert_equal(ret, exit_code)
+        return output
 
 
     def create_src_archive(self, name):
     def create_src_archive(self, name):
         self.cmd('create', self.repository_location + '::' + name, src_dir)
         self.cmd('create', self.repository_location + '::' + name, src_dir)
@@ -231,9 +282,37 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         shutil.rmtree(self.cache_path)
         shutil.rmtree(self.cache_path)
         with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
         with environment_variable(BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK='1'):
             info_output2 = self.cmd('info', self.repository_location + '::test')
             info_output2 = self.cmd('info', self.repository_location + '::test')
-        # info_output2 starts with some "initializing cache" text but should
-        # end the same way as info_output
-        assert info_output2.endswith(info_output)
+
+        def filter(output):
+            # filter for interesting "info" output, ignore cache rebuilding related stuff
+            prefixes = ['Name:', 'Fingerprint:', 'Number of files:', 'This archive:',
+                        'All archives:', 'Chunk index:', ]
+            result = []
+            for line in output.splitlines():
+                for prefix in prefixes:
+                    if line.startswith(prefix):
+                        result.append(line)
+            return '\n'.join(result)
+
+        # the interesting parts of info_output2 and info_output should be same
+        self.assert_equal(filter(info_output), filter(info_output2))
+
+    def test_atime(self):
+        have_root = self.create_test_files()
+        atime, mtime = 123456780, 234567890
+        os.utime('input/file1', (atime, mtime))
+        self.cmd('init', self.repository_location)
+        self.cmd('create', self.repository_location + '::test', 'input')
+        with changedir('output'):
+            self.cmd('extract', self.repository_location + '::test')
+        sti = os.stat('input/file1')
+        sto = os.stat('output/input/file1')
+        assert st_mtime_ns(sti) == st_mtime_ns(sto) == mtime * 1e9
+        if hasattr(os, 'O_NOATIME'):
+            assert st_atime_ns(sti) == st_atime_ns(sto) == atime * 1e9
+        else:
+            # it touched the input file's atime while backing it up
+            assert st_atime_ns(sto) == atime * 1e9
 
 
     def _extract_repository_id(self, path):
     def _extract_repository_id(self, path):
         return Repository(self.repository_path).id
         return Repository(self.repository_path).id
@@ -304,7 +383,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('init', '--encryption=none', self.repository_location)
         self.cmd('init', '--encryption=none', self.repository_location)
         self._set_repository_id(self.repository_path, repository_id)
         self._set_repository_id(self.repository_path, repository_id)
         self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
         self.assert_equal(repository_id, self._extract_repository_id(self.repository_path))
-        self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
+        if self.FORK_DEFAULT:
+            self.cmd('create', self.repository_location + '::test.2', 'input', exit_code=1)  # fails
+        else:
+            self.assert_raises(Cache.EncryptionMethodMismatch, lambda: self.cmd('create', self.repository_location + '::test.2', 'input'))
 
 
     def test_repository_swap_detection2(self):
     def test_repository_swap_detection2(self):
         self.create_test_files()
         self.create_test_files()
@@ -314,7 +396,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('create', self.repository_location + '_encrypted::test', 'input')
         self.cmd('create', self.repository_location + '_encrypted::test', 'input')
         shutil.rmtree(self.repository_path + '_encrypted')
         shutil.rmtree(self.repository_path + '_encrypted')
         os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
         os.rename(self.repository_path + '_unencrypted', self.repository_path + '_encrypted')
-        self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
+        if self.FORK_DEFAULT:
+            self.cmd('create', self.repository_location + '_encrypted::test.2', 'input', exit_code=1)  # fails
+        else:
+            self.assert_raises(Cache.RepositoryAccessAborted, lambda: self.cmd('create', self.repository_location + '_encrypted::test.2', 'input'))
 
 
     def test_strip_components(self):
     def test_strip_components(self):
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)
@@ -539,8 +624,12 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.assert_in('bar-2015-08-12-20:00', output)
         self.assert_in('bar-2015-08-12-20:00', output)
 
 
     def test_usage(self):
     def test_usage(self):
-        self.assert_raises(SystemExit, lambda: self.cmd())
-        self.assert_raises(SystemExit, lambda: self.cmd('-h'))
+        if self.FORK_DEFAULT:
+            self.cmd(exit_code=0)
+            self.cmd('-h', exit_code=0)
+        else:
+            self.assert_raises(SystemExit, lambda: self.cmd())
+            self.assert_raises(SystemExit, lambda: self.cmd('-h'))
 
 
     def test_help(self):
     def test_help(self):
         assert 'Borg' in self.cmd('help')
         assert 'Borg' in self.cmd('help')
@@ -627,6 +716,12 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.verify_aes_counter_uniqueness('passphrase')
         self.verify_aes_counter_uniqueness('passphrase')
 
 
 
 
+@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
+class ArchiverTestCaseBinary(ArchiverTestCase):
+    EXE = 'borg.exe'
+    FORK_DEFAULT = True
+
+
 class ArchiverCheckTestCase(ArchiverTestCaseBase):
 class ArchiverCheckTestCase(ArchiverTestCaseBase):
 
 
     def setUp(self):
     def setUp(self):
@@ -716,3 +811,16 @@ if 0:
                 self.cmd('init', self.repository_location + '_2')
                 self.cmd('init', self.repository_location + '_2')
             with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
             with patch.object(RemoteRepository, 'extra_test_args', ['--restrict-to-path', '/foo', '--restrict-to-path', path_prefix]):
                 self.cmd('init', self.repository_location + '_3')
                 self.cmd('init', self.repository_location + '_3')
+
+        # skip fuse tests here, they deadlock since this change in exec_cmd:
+        # -output = subprocess.check_output(borg + args, stderr=None)
+        # +output = subprocess.check_output(borg + args, stderr=subprocess.STDOUT)
+        # this was introduced because some tests expect stderr contents to show up
+        # in "output" also. Also, the non-forking exec_cmd catches both, too.
+        @unittest.skip('deadlock issues')
+        def test_fuse_mount_repository(self):
+            pass
+
+        @unittest.skip('deadlock issues')
+        def test_fuse_mount_archive(self):
+            pass

+ 100 - 0
borg/testsuite/benchmark.py

@@ -0,0 +1,100 @@
+"""
+Do benchmarks using pytest-benchmark.
+
+Usage:
+
+    py.test --benchmark-only
+"""
+
+import os
+
+import pytest
+
+from .archiver import changedir, cmd
+
+
+@pytest.yield_fixture
+def repo_url(request, tmpdir):
+    os.environ['BORG_PASSPHRASE'] = '123456'
+    os.environ['BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
+    os.environ['BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'] = '1'
+    os.environ['BORG_KEYS_DIR'] = str(tmpdir.join('keys'))
+    os.environ['BORG_CACHE_DIR'] = str(tmpdir.join('cache'))
+    yield str(tmpdir.join('repository'))
+    tmpdir.remove(rec=1)
+
+
+@pytest.fixture(params=["none", "passphrase"])
+def repo(request, cmd, repo_url):
+    cmd('init', '--encryption', request.param, repo_url)
+    return repo_url
+
+
+@pytest.yield_fixture(scope='session', params=["zeros", "random"])
+def testdata(request, tmpdir_factory):
+    count, size = 10, 1000*1000
+    p = tmpdir_factory.mktemp('data')
+    data_type = request.param
+    if data_type == 'zeros':
+        # do not use a binary zero (\0) to avoid sparse detection
+        data = lambda: b'0' * size
+    if data_type == 'random':
+        rnd = open('/dev/urandom', 'rb')
+        data = lambda: rnd.read(size)
+    for i in range(count):
+        with open(str(p.join(str(i))), "wb") as f:
+            f.write(data())
+    if data_type == 'random':
+        rnd.close()
+    yield str(p)
+    p.remove(rec=1)
+
+
+@pytest.fixture(params=['none', 'lz4'])
+def archive(request, cmd, repo, testdata):
+    archive_url = repo + '::test'
+    cmd('create', '--compression', request.param, archive_url, testdata)
+    return archive_url
+
+
+def test_create_none(benchmark, cmd, repo, testdata):
+    result, out = benchmark.pedantic(cmd, ('create', '--compression', 'none', repo + '::test', testdata))
+    assert result == 0
+
+
+def test_create_lz4(benchmark, cmd, repo, testdata):
+    result, out = benchmark.pedantic(cmd, ('create', '--compression', 'lz4', repo + '::test', testdata))
+    assert result == 0
+
+
+def test_extract(benchmark, cmd, archive, tmpdir):
+    with changedir(str(tmpdir)):
+        result, out = benchmark.pedantic(cmd, ('extract', archive))
+    assert result == 0
+
+
+def test_delete(benchmark, cmd, archive):
+    result, out = benchmark.pedantic(cmd, ('delete', archive))
+    assert result == 0
+
+
+def test_list(benchmark, cmd, archive):
+    result, out = benchmark(cmd, 'list', archive)
+    assert result == 0
+
+
+def test_info(benchmark, cmd, archive):
+    result, out = benchmark(cmd, 'info', archive)
+    assert result == 0
+
+
+def test_check(benchmark, cmd, archive):
+    repo = archive.split('::')[0]
+    result, out = benchmark(cmd, 'check', repo)
+    assert result == 0
+
+
+def test_help(benchmark, cmd):
+    result, out = benchmark(cmd, 'help')
+    assert result == 0
+

+ 111 - 30
borg/testsuite/helpers.py

@@ -1,14 +1,15 @@
 import hashlib
 import hashlib
 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
 import sys
 import sys
 import msgpack
 import msgpack
 
 
-from ..helpers import adjust_patterns, exclude_path, Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, \
-    prune_within, prune_split, get_cache_dir, \
+from ..helpers import adjust_patterns, exclude_path, Location, format_file_size, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, \
+    prune_within, prune_split, get_cache_dir, Statistics, \
     StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams
     StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams
 from . import BaseTestCase
 from . import BaseTestCase
 
 
@@ -29,44 +30,44 @@ class TestLocationWithoutEnv:
     def test_ssh(self, monkeypatch):
     def test_ssh(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('ssh://user@host:1234/some/path::archive')) == \
         assert repr(Location('ssh://user@host:1234/some/path::archive')) == \
-               "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
+            "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
         assert repr(Location('ssh://user@host:1234/some/path')) == \
         assert repr(Location('ssh://user@host:1234/some/path')) == \
-               "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
+            "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
 
 
     def test_file(self, monkeypatch):
     def test_file(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('file:///some/path::archive')) == \
         assert repr(Location('file:///some/path::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
         assert repr(Location('file:///some/path')) == \
         assert repr(Location('file:///some/path')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
 
 
     def test_scp(self, monkeypatch):
     def test_scp(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('user@host:/some/path::archive')) == \
         assert repr(Location('user@host:/some/path::archive')) == \
-               "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
+            "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
         assert repr(Location('user@host:/some/path')) == \
         assert repr(Location('user@host:/some/path')) == \
-               "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
+            "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
 
 
     def test_folder(self, monkeypatch):
     def test_folder(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('path::archive')) == \
         assert repr(Location('path::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
         assert repr(Location('path')) == \
         assert repr(Location('path')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
 
 
     def test_abspath(self, monkeypatch):
     def test_abspath(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('/some/absolute/path::archive')) == \
         assert repr(Location('/some/absolute/path::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
         assert repr(Location('/some/absolute/path')) == \
         assert repr(Location('/some/absolute/path')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
 
 
     def test_relpath(self, monkeypatch):
     def test_relpath(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
         assert repr(Location('some/relative/path::archive')) == \
         assert repr(Location('some/relative/path::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
         assert repr(Location('some/relative/path')) == \
         assert repr(Location('some/relative/path')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
 
 
     def test_underspecified(self, monkeypatch):
     def test_underspecified(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -94,51 +95,51 @@ class TestLocationWithoutEnv:
                      'ssh://user@host:1234/some/path::archive']
                      'ssh://user@host:1234/some/path::archive']
         for location in locations:
         for location in locations:
             assert Location(location).canonical_path() == \
             assert Location(location).canonical_path() == \
-                   Location(Location(location).canonical_path()).canonical_path()
+                Location(Location(location).canonical_path()).canonical_path()
 
 
 
 
 class TestLocationWithEnv:
 class TestLocationWithEnv:
     def test_ssh(self, monkeypatch):
     def test_ssh(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path')
         monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
+            "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
+            "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
 
 
     def test_file(self, monkeypatch):
     def test_file(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', 'file:///some/path')
         monkeypatch.setenv('BORG_REPO', 'file:///some/path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
 
 
     def test_scp(self, monkeypatch):
     def test_scp(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', 'user@host:/some/path')
         monkeypatch.setenv('BORG_REPO', 'user@host:/some/path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
+            "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
+            "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
 
 
     def test_folder(self, monkeypatch):
     def test_folder(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', 'path')
         monkeypatch.setenv('BORG_REPO', 'path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
 
 
     def test_abspath(self, monkeypatch):
     def test_abspath(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
         monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
 
 
     def test_relpath(self, monkeypatch):
     def test_relpath(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', 'some/relative/path')
         monkeypatch.setenv('BORG_REPO', 'some/relative/path')
         assert repr(Location('::archive')) == \
         assert repr(Location('::archive')) == \
-               "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
+            "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
         assert repr(Location()) == \
         assert repr(Location()) == \
-               "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
+            "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
 
 
     def test_no_slashes(self, monkeypatch):
     def test_no_slashes(self, monkeypatch):
         monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
         monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
@@ -211,7 +212,7 @@ class PatternNonAsciiTestCase(BaseTestCase):
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert not e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert not e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
-    
+
     def testInvalidUnicode(self):
     def testInvalidUnicode(self):
         pattern = str(b'ba\x80', 'latin1')
         pattern = str(b'ba\x80', 'latin1')
         i = IncludePattern(pattern)
         i = IncludePattern(pattern)
@@ -234,7 +235,7 @@ class OSXPatternNormalizationTestCase(BaseTestCase):
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
-    
+
     def testDecomposedUnicode(self):
     def testDecomposedUnicode(self):
         pattern = 'ba\N{COMBINING ACUTE ACCENT}'
         pattern = 'ba\N{COMBINING ACUTE ACCENT}'
         i = IncludePattern(pattern)
         i = IncludePattern(pattern)
@@ -244,7 +245,7 @@ class OSXPatternNormalizationTestCase(BaseTestCase):
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert i.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert e.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
         assert e.match("ba\N{COMBINING ACUTE ACCENT}/foo")
-    
+
     def testInvalidUnicode(self):
     def testInvalidUnicode(self):
         pattern = str(b'ba\x80', 'latin1')
         pattern = str(b'ba\x80', 'latin1')
         i = IncludePattern(pattern)
         i = IncludePattern(pattern)
@@ -399,3 +400,83 @@ def test_get_cache_dir():
     # reset old env
     # reset old env
     if old_env is not None:
     if old_env is not None:
         os.environ['BORG_CACHE_DIR'] = old_env
         os.environ['BORG_CACHE_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():
+    """test the size formatting routines"""
+    si_size_map = {
+        0: '0 B',  # no rounding necessary for those
+        1: '1 B',
+        142: '142 B',
+        999: '999 B',
+        1000: '1.00 kB',  # rounding starts here
+        1001: '1.00 kB',  # should be rounded away
+        1234: '1.23 kB',  # should be rounded down
+        1235: '1.24 kB',  # should be rounded up
+        1010: '1.01 kB',  # rounded down as well
+        999990000: '999.99 MB',  # rounded down
+        999990001: '999.99 MB',  # rounded down
+        999995000: '1.00 GB',  # rounded up to next unit
+        10**6: '1.00 MB',  # and all the remaining units, megabytes
+        10**9: '1.00 GB',  # gigabytes
+        10**12: '1.00 TB',  # terabytes
+        10**15: '1.00 PB',  # petabytes
+        10**18: '1.00 EB',  # exabytes
+        10**21: '1.00 ZB',  # zottabytes
+        10**24: '1.00 YB',  # yottabytes
+    }
+    for size, fmt in si_size_map.items():
+        assert format_file_size(size) == fmt
+
+
+def test_file_size_precision():
+    assert format_file_size(1234, precision=1) == '1.2 kB'  # rounded down
+    assert format_file_size(1254, precision=1) == '1.3 kB'  # rounded up
+    assert format_file_size(999990000, precision=1) == '1.0 GB'  # and not 999.9 MB or 1000.0 MB

+ 40 - 0
borg/testsuite/logger.py

@@ -0,0 +1,40 @@
+import logging
+from io import StringIO
+
+from mock import Mock
+import pytest
+
+from ..logger import find_parent_module, create_logger, setup_logging
+logger = create_logger()
+
+
+@pytest.fixture()
+def io_logger():
+    io = StringIO()
+    handler = setup_logging(io)
+    handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
+    logger.setLevel(logging.DEBUG)
+    return io
+
+
+def test_setup_logging(io_logger):
+    logger.info('hello world')
+    assert io_logger.getvalue() == "borg.testsuite.logger: hello world\n"
+
+
+def test_multiple_loggers(io_logger):
+    logger = logging.getLogger(__name__)
+    logger.info('hello world 1')
+    assert io_logger.getvalue() == "borg.testsuite.logger: hello world 1\n"
+    logger = logging.getLogger('borg.testsuite.logger')
+    logger.info('hello world 2')
+    assert io_logger.getvalue() == "borg.testsuite.logger: hello world 1\nborg.testsuite.logger: hello world 2\n"
+    io_logger.truncate(0)
+    io_logger.seek(0)
+    logger = logging.getLogger('borg.testsuite.logger')
+    logger.info('hello world 2')
+    assert io_logger.getvalue() == "borg.testsuite.logger: hello world 2\n"
+
+
+def test_parent_module():
+    assert find_parent_module() == __name__

+ 36 - 0
borg/testsuite/platform.py

@@ -72,6 +72,42 @@ class PlatformLinuxTestCase(BaseTestCase):
         self.assert_equal(self.get_acl(self.tmpdir)[b'acl_access'], ACCESS_ACL)
         self.assert_equal(self.get_acl(self.tmpdir)[b'acl_access'], ACCESS_ACL)
         self.assert_equal(self.get_acl(self.tmpdir)[b'acl_default'], DEFAULT_ACL)
         self.assert_equal(self.get_acl(self.tmpdir)[b'acl_default'], DEFAULT_ACL)
 
 
+    def test_non_ascii_acl(self):
+        # Testing non-ascii ACL processing to see whether our code is robust.
+        # I have no idea whether non-ascii ACLs are allowed by the standard,
+        # but in practice they seem to be out there and must not make our code explode.
+        file = tempfile.NamedTemporaryFile()
+        self.assert_equal(self.get_acl(file.name), {})
+        nothing_special = 'user::rw-\ngroup::r--\nmask::rw-\nother::---\n'.encode('ascii')
+        # TODO: can this be tested without having an existing system user übel with uid 666 gid 666?
+        user_entry = 'user:übel:rw-:666'.encode('utf-8')
+        user_entry_numeric = 'user:666:rw-:666'.encode('ascii')
+        group_entry = 'group:übel:rw-:666'.encode('utf-8')
+        group_entry_numeric = 'group:666:rw-:666'.encode('ascii')
+        acl = b'\n'.join([nothing_special, user_entry, group_entry])
+        self.set_acl(file.name, access=acl, numeric_owner=False)
+        acl_access = self.get_acl(file.name, numeric_owner=False)[b'acl_access']
+        self.assert_in(user_entry, acl_access)
+        self.assert_in(group_entry, acl_access)
+        acl_access_numeric = self.get_acl(file.name, numeric_owner=True)[b'acl_access']
+        self.assert_in(user_entry_numeric, acl_access_numeric)
+        self.assert_in(group_entry_numeric, acl_access_numeric)
+        file2 = tempfile.NamedTemporaryFile()
+        self.set_acl(file2.name, access=acl, numeric_owner=True)
+        acl_access = self.get_acl(file2.name, numeric_owner=False)[b'acl_access']
+        self.assert_in(user_entry, acl_access)
+        self.assert_in(group_entry, acl_access)
+        acl_access_numeric = self.get_acl(file.name, numeric_owner=True)[b'acl_access']
+        self.assert_in(user_entry_numeric, acl_access_numeric)
+        self.assert_in(group_entry_numeric, acl_access_numeric)
+
+    def test_utils(self):
+        from ..platform_linux import acl_use_local_uid_gid
+        self.assert_equal(acl_use_local_uid_gid(b'user:nonexistent1234:rw-:1234'), b'user:1234:rw-')
+        self.assert_equal(acl_use_local_uid_gid(b'group:nonexistent1234:rw-:1234'), b'group:1234:rw-')
+        self.assert_equal(acl_use_local_uid_gid(b'user:root:rw-:0'), b'user:0:rw-')
+        self.assert_equal(acl_use_local_uid_gid(b'group:root:rw-:0'), b'group:0:rw-')
+
 
 
 @unittest.skipUnless(sys.platform.startswith('darwin'), 'OS X only test')
 @unittest.skipUnless(sys.platform.startswith('darwin'), 'OS X only test')
 @unittest.skipIf(fakeroot_detected(), 'not compatible with fakeroot')
 @unittest.skipIf(fakeroot_detected(), 'not compatible with fakeroot')

+ 57 - 12
borg/testsuite/upgrader.py

@@ -1,6 +1,4 @@
 import os
 import os
-import shutil
-import tempfile
 
 
 import pytest
 import pytest
 
 
@@ -14,10 +12,8 @@ except ImportError:
 from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey
 from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey
 from ..helpers import get_keys_dir
 from ..helpers import get_keys_dir
 from ..key import KeyfileKey
 from ..key import KeyfileKey
-from ..repository import Repository, MAGIC
-
-pytestmark = pytest.mark.skipif(attic is None,
-                                reason='cannot find an attic install')
+from ..remote import RemoteRepository
+from ..repository import Repository
 
 
 
 
 def repo_valid(path):
 def repo_valid(path):
@@ -64,7 +60,13 @@ def attic_repo(tmpdir):
     return attic_repo
     return attic_repo
 
 
 
 
-def test_convert_segments(tmpdir, attic_repo):
+@pytest.fixture(params=[True, False])
+def inplace(request):
+    return request.param
+
+
+@pytest.mark.skipif(attic is None, reason='cannot find an attic install')
+def test_convert_segments(tmpdir, attic_repo, inplace):
     """test segment conversion
     """test segment conversion
 
 
     this will load the given attic repository, list all the segments
     this will load the given attic repository, list all the segments
@@ -77,11 +79,10 @@ def test_convert_segments(tmpdir, attic_repo):
     """
     """
     # check should fail because of magic number
     # check should fail because of magic number
     assert not repo_valid(tmpdir)
     assert not repo_valid(tmpdir)
-    print("opening attic repository with borg and converting")
     repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
     repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
     segments = [filename for i, filename in repo.io.segment_iterator()]
     segments = [filename for i, filename in repo.io.segment_iterator()]
     repo.close()
     repo.close()
-    repo.convert_segments(segments, dryrun=False)
+    repo.convert_segments(segments, dryrun=False, inplace=inplace)
     repo.convert_cache(dryrun=False)
     repo.convert_cache(dryrun=False)
     assert repo_valid(tmpdir)
     assert repo_valid(tmpdir)
 
 
@@ -124,6 +125,7 @@ def attic_key_file(attic_repo, tmpdir):
                                        MockArgs(keys_dir))
                                        MockArgs(keys_dir))
 
 
 
 
+@pytest.mark.skipif(attic is None, reason='cannot find an attic install')
 def test_keys(tmpdir, attic_repo, attic_key_file):
 def test_keys(tmpdir, attic_repo, attic_key_file):
     """test key conversion
     """test key conversion
 
 
@@ -142,7 +144,8 @@ def test_keys(tmpdir, attic_repo, attic_key_file):
     assert key_valid(attic_key_file.path)
     assert key_valid(attic_key_file.path)
 
 
 
 
-def test_convert_all(tmpdir, attic_repo, attic_key_file):
+@pytest.mark.skipif(attic is None, reason='cannot find an attic install')
+def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace):
     """test all conversion steps
     """test all conversion steps
 
 
     this runs everything. mostly redundant test, since everything is
     this runs everything. mostly redundant test, since everything is
@@ -156,8 +159,50 @@ def test_convert_all(tmpdir, attic_repo, attic_key_file):
     """
     """
     # check should fail because of magic number
     # check should fail because of magic number
     assert not repo_valid(tmpdir)
     assert not repo_valid(tmpdir)
-    print("opening attic repository with borg and converting")
+
+    def stat_segment(path):
+        return os.stat(os.path.join(path, 'data', '0', '0'))
+
+    def first_inode(path):
+        return stat_segment(path).st_ino
+
+    orig_inode = first_inode(attic_repo.path)
     repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
     repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
-    repo.upgrade(dryrun=False)
+    # replicate command dispatch, partly
+    os.umask(RemoteRepository.umask)
+    backup = repo.upgrade(dryrun=False, inplace=inplace)
+    if inplace:
+        assert backup is None
+        assert first_inode(repo.path) == orig_inode
+    else:
+        assert backup
+        assert first_inode(repo.path) != first_inode(backup)
+        # i have seen cases where the copied tree has world-readable
+        # permissions, which is wrong
+        assert stat_segment(backup).st_mode & 0o007 == 0
+
     assert key_valid(attic_key_file.path)
     assert key_valid(attic_key_file.path)
     assert repo_valid(tmpdir)
     assert repo_valid(tmpdir)
+
+
+def test_hardlink(tmpdir, inplace):
+    """test that we handle hard links properly
+
+    that is, if we are in "inplace" mode, hardlinks should *not*
+    change (ie. we write to the file directly, so we do not rewrite the
+    whole file, and we do not re-create the file).
+
+    if we are *not* in inplace mode, then the inode should change, as
+    we are supposed to leave the original inode alone."""
+    a = str(tmpdir.join('a'))
+    with open(a, 'wb') as tmp:
+        tmp.write(b'aXXX')
+    b = str(tmpdir.join('b'))
+    os.link(a, b)
+    AtticRepositoryUpgrader.header_replace(b, b'a', b'b', inplace=inplace)
+    if not inplace:
+        assert os.stat(a).st_ino != os.stat(b).st_ino
+    else:
+        assert os.stat(a).st_ino == os.stat(b).st_ino
+    with open(b, 'rb') as tmp:
+        assert tmp.read() == b'bXXX'

+ 56 - 31
borg/upgrader.py

@@ -1,6 +1,10 @@
 from binascii import hexlify
 from binascii import hexlify
+import datetime
+import logging
+logger = logging.getLogger(__name__)
 import os
 import os
 import shutil
 import shutil
+import sys
 import time
 import time
 
 
 from .helpers import get_keys_dir, get_cache_dir
 from .helpers import get_keys_dir, get_cache_dir
@@ -12,7 +16,7 @@ ATTIC_MAGIC = b'ATTICSEG'
 
 
 
 
 class AtticRepositoryUpgrader(Repository):
 class AtticRepositoryUpgrader(Repository):
-    def upgrade(self, dryrun=True):
+    def upgrade(self, dryrun=True, inplace=False):
         """convert an attic repository to a borg repository
         """convert an attic repository to a borg repository
 
 
         those are the files that need to be upgraded here, from most
         those are the files that need to be upgraded here, from most
@@ -23,14 +27,20 @@ class AtticRepositoryUpgrader(Repository):
         we nevertheless do the order in reverse, as we prefer to do
         we nevertheless do the order in reverse, as we prefer to do
         the fast stuff first, to improve interactivity.
         the fast stuff first, to improve interactivity.
         """
         """
-        print("reading segments from attic repository using borg")
-        # we need to open it to load the configuration and other fields
+        backup = None
+        if not inplace:
+            backup = '{}.upgrade-{:%Y-%m-%d-%H:%M:%S}'.format(self.path, datetime.datetime.now())
+            logger.info('making a hardlink copy in %s', backup)
+            if not dryrun:
+                shutil.copytree(self.path, backup, copy_function=os.link)
+        logger.info("opening attic repository with borg and converting")
+        # we need to open the repo to load configuration, keyfiles and segments
         self.open(self.path, exclusive=False)
         self.open(self.path, exclusive=False)
         segments = [filename for i, filename in self.io.segment_iterator()]
         segments = [filename for i, filename in self.io.segment_iterator()]
         try:
         try:
             keyfile = self.find_attic_keyfile()
             keyfile = self.find_attic_keyfile()
         except KeyfileNotFoundError:
         except KeyfileNotFoundError:
-            print("no key file found for repository")
+            logger.warning("no key file found for repository")
         else:
         else:
             self.convert_keyfiles(keyfile, dryrun)
             self.convert_keyfiles(keyfile, dryrun)
         self.close()
         self.close()
@@ -39,13 +49,14 @@ class AtticRepositoryUpgrader(Repository):
                                    exclusive=True).acquire()
                                    exclusive=True).acquire()
         try:
         try:
             self.convert_cache(dryrun)
             self.convert_cache(dryrun)
-            self.convert_segments(segments, dryrun)
+            self.convert_segments(segments, dryrun=dryrun, inplace=inplace)
         finally:
         finally:
             self.lock.release()
             self.lock.release()
             self.lock = None
             self.lock = None
+        return backup
 
 
     @staticmethod
     @staticmethod
-    def convert_segments(segments, dryrun):
+    def convert_segments(segments, dryrun=True, inplace=False):
         """convert repository segments from attic to borg
         """convert repository segments from attic to borg
 
 
         replacement pattern is `s/ATTICSEG/BORG_SEG/` in files in
         replacement pattern is `s/ATTICSEG/BORG_SEG/` in files in
@@ -53,26 +64,39 @@ class AtticRepositoryUpgrader(Repository):
 
 
         luckily the magic string length didn't change so we can just
         luckily the magic string length didn't change so we can just
         replace the 8 first bytes of all regular files in there."""
         replace the 8 first bytes of all regular files in there."""
-        print("converting %d segments..." % len(segments))
+        logger.info("converting %d segments..." % len(segments))
         i = 0
         i = 0
         for filename in segments:
         for filename in segments:
             i += 1
             i += 1
-            print("\rconverting segment %d/%d in place, %.2f%% done (%s)"
-                  % (i, len(segments), 100*float(i)/len(segments), filename), end='')
+            print("\rconverting segment %d/%d, %.2f%% done (%s)"
+                  % (i, len(segments), 100*float(i)/len(segments), filename),
+                  end='', file=sys.stderr)
             if dryrun:
             if dryrun:
                 time.sleep(0.001)
                 time.sleep(0.001)
             else:
             else:
-                AtticRepositoryUpgrader.header_replace(filename, ATTIC_MAGIC, MAGIC)
-        print()
+                AtticRepositoryUpgrader.header_replace(filename, ATTIC_MAGIC, MAGIC, inplace=inplace)
+        print(file=sys.stderr)
 
 
     @staticmethod
     @staticmethod
-    def header_replace(filename, old_magic, new_magic):
+    def header_replace(filename, old_magic, new_magic, inplace=True):
         with open(filename, 'r+b') as segment:
         with open(filename, 'r+b') as segment:
             segment.seek(0)
             segment.seek(0)
             # only write if necessary
             # only write if necessary
             if segment.read(len(old_magic)) == old_magic:
             if segment.read(len(old_magic)) == old_magic:
-                segment.seek(0)
-                segment.write(new_magic)
+                if inplace:
+                    segment.seek(0)
+                    segment.write(new_magic)
+                else:
+                    # rename the hardlink and rewrite the file. this works
+                    # because the file is still open. so even though the file
+                    # is renamed, we can still read it until it is closed.
+                    os.rename(filename, filename + '.tmp')
+                    with open(filename, 'wb') as new_segment:
+                        new_segment.write(new_magic)
+                        new_segment.write(segment.read())
+                    # the little dance with the .tmp file is necessary
+                    # because Windows won't allow overwriting an open file.
+                    os.unlink(filename + '.tmp')
 
 
     def find_attic_keyfile(self):
     def find_attic_keyfile(self):
         """find the attic keyfiles
         """find the attic keyfiles
@@ -107,12 +131,12 @@ class AtticRepositoryUpgrader(Repository):
         key file because magic string length changed, but that's not a
         key file because magic string length changed, but that's not a
         problem because the keyfiles are small (compared to, say,
         problem because the keyfiles are small (compared to, say,
         all the segments)."""
         all the segments)."""
-        print("converting keyfile %s" % keyfile)
+        logger.info("converting keyfile %s" % keyfile)
         with open(keyfile, 'r') as f:
         with open(keyfile, 'r') as f:
             data = f.read()
             data = f.read()
         data = data.replace(AtticKeyfileKey.FILE_ID, KeyfileKey.FILE_ID, 1)
         data = data.replace(AtticKeyfileKey.FILE_ID, KeyfileKey.FILE_ID, 1)
         keyfile = os.path.join(get_keys_dir(), os.path.basename(keyfile))
         keyfile = os.path.join(get_keys_dir(), os.path.basename(keyfile))
-        print("writing borg keyfile to %s" % keyfile)
+        logger.info("writing borg keyfile to %s" % keyfile)
         if not dryrun:
         if not dryrun:
             with open(keyfile, 'w') as f:
             with open(keyfile, 'w') as f:
                 f.write(data)
                 f.write(data)
@@ -135,12 +159,14 @@ class AtticRepositoryUpgrader(Repository):
           `Cache.open()`, edit in place and then `Cache.close()` to
           `Cache.open()`, edit in place and then `Cache.close()` to
           make sure we have locking right
           make sure we have locking right
         """
         """
-        caches = []
         transaction_id = self.get_index_transaction_id()
         transaction_id = self.get_index_transaction_id()
         if transaction_id is None:
         if transaction_id is None:
-            print('no index file found for repository %s' % self.path)
+            logger.warning('no index file found for repository %s' % self.path)
         else:
         else:
-            caches += [os.path.join(self.path, 'index.%d' % transaction_id).encode('utf-8')]
+            index = os.path.join(self.path, 'index.%d' % transaction_id).encode('utf-8')
+            logger.info("converting index index %s" % index)
+            if not dryrun:
+                AtticRepositoryUpgrader.header_replace(index, b'ATTICIDX', b'BORG_IDX')
 
 
         # copy of attic's get_cache_dir()
         # copy of attic's get_cache_dir()
         attic_cache_dir = os.environ.get('ATTIC_CACHE_DIR',
         attic_cache_dir = os.environ.get('ATTIC_CACHE_DIR',
@@ -160,23 +186,23 @@ class AtticRepositoryUpgrader(Repository):
             :params path: the basename of the cache file to copy
             :params path: the basename of the cache file to copy
             (example: "files" or "chunks") as a string
             (example: "files" or "chunks") as a string
 
 
-            :returns: the borg file that was created or None if non
-            was created.
+            :returns: the borg file that was created or None if no
+            Attic cache file was found.
 
 
             """
             """
             attic_file = os.path.join(attic_cache_dir, path)
             attic_file = os.path.join(attic_cache_dir, path)
             if os.path.exists(attic_file):
             if os.path.exists(attic_file):
                 borg_file = os.path.join(borg_cache_dir, path)
                 borg_file = os.path.join(borg_cache_dir, path)
                 if os.path.exists(borg_file):
                 if os.path.exists(borg_file):
-                    print("borg cache file already exists in %s, skipping conversion of %s" % (borg_file, attic_file))
+                    logger.warning("borg cache file already exists in %s, not copying from Attic", borg_file)
                 else:
                 else:
-                    print("copying attic cache file from %s to %s" % (attic_file, borg_file))
+                    logger.info("copying attic cache file from %s to %s" % (attic_file, borg_file))
                     if not dryrun:
                     if not dryrun:
                         shutil.copyfile(attic_file, borg_file)
                         shutil.copyfile(attic_file, borg_file)
-                    return borg_file
+                return borg_file
             else:
             else:
-                print("no %s cache file found in %s" % (path, attic_file))
-            return None
+                logger.warning("no %s cache file found in %s" % (path, attic_file))
+                return None
 
 
         # XXX: untested, because generating cache files is a PITA, see
         # XXX: untested, because generating cache files is a PITA, see
         # Archiver.do_create() for proof
         # Archiver.do_create() for proof
@@ -190,11 +216,10 @@ class AtticRepositoryUpgrader(Repository):
 
 
             # we need to convert the headers of those files, copy first
             # we need to convert the headers of those files, copy first
             for cache in ['chunks']:
             for cache in ['chunks']:
-                copied = copy_cache_file(cache)
-                if copied:
-                    print("converting cache %s" % cache)
-                    if not dryrun:
-                        AtticRepositoryUpgrader.header_replace(cache, b'ATTICIDX', b'BORG_IDX')
+                cache = copy_cache_file(cache)
+                logger.info("converting cache %s" % cache)
+                if not dryrun:
+                    AtticRepositoryUpgrader.header_replace(cache, b'ATTICIDX', b'BORG_IDX')
 
 
 
 
 class AtticKeyfileKey(KeyfileKey):
 class AtticKeyfileKey(KeyfileKey):

+ 1 - 41
docs/Makefile

@@ -36,7 +36,7 @@ help:
 clean:
 clean:
 	-rm -rf $(BUILDDIR)/*
 	-rm -rf $(BUILDDIR)/*
 
 
-html: usage api.rst
+html:
 	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 	@echo
 	@echo
 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
 	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
@@ -128,43 +128,3 @@ doctest:
 	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
 	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
 	@echo "Testing of doctests in the sources finished, look at the " \
 	@echo "Testing of doctests in the sources finished, look at the " \
 	      "results in $(BUILDDIR)/doctest/output.txt."
 	      "results in $(BUILDDIR)/doctest/output.txt."
-
-gh-io: html
-	GH_IO_CLONE="`mktemp -d`" && \
-    git clone git@github.com:borgbackup/borgbackup.github.io.git $$GH_IO_CLONE && \
-	(cd $$GH_IO_CLONE && git rm -r *) && \
-	cp -r _build/html/* $$GH_IO_CLONE && \
-	(cd $$GH_IO_CLONE && git add -A && git commit -m 'Updated borgbackup.github.io' && git push) && \
-	rm -rf $$GH_IO_CLONE
-
-inotify: html
-	while inotifywait -r . --exclude usage.rst --exclude '_build/*' ; do make html ; done
-
-# generate list of targets
-usage: $(shell borg help | grep -A1 "Available commands:" | tail -1 | sed 's/[{} ]//g;s/,\|^/.rst.inc usage\//g;s/^.rst.inc//;s/usage\/help//')
-
-# generate help file based on usage
-usage/%.rst.inc: ../borg/archiver.py
-	@echo generating usage for $*
-	@printf ".. _borg_$*:\n\n" > $@
-	@printf "borg $*\n" >> $@
-	@echo -n borg $* | tr 'a-z- ' '-' >> $@
-	@printf "\n::\n\n" >> $@
-	@borg help $* --usage-only | sed -e 's/^/    /' >> $@
-	@printf "\nDescription\n~~~~~~~~~~~\n" >> $@
-	@borg help $* --epilog-only >> $@
-
-api.rst: Makefile
-	@echo "auto-generating API documentation"
-	@echo "Borg Backup API documentation" > $@
-	@echo "=============================" >> $@
-	@echo "" >> $@
-	@for mod in ../borg/*.pyx ../borg/*.py; do \
-		if echo "$$mod" | grep -q "/_"; then \
-			continue ; \
-		fi ; \
-		printf ".. automodule:: "; \
-		echo "$$mod" | sed "s!\.\./!!;s/\.pyx\?//;s!/!.!"; \
-		echo "    :members:"; \
-		echo "    :undoc-members:"; \
-	done >> $@

+ 0 - 0
docs/_themes/local/static/favicon.ico → docs/_static/favicon.ico


+ 0 - 5
docs/_themes/local/sidebarlogo.html

@@ -1,5 +0,0 @@
-<div class="sidebarlogo">
-  <a href="{{ pathto('index') }}">
-  <div class="title">Borg</div>
-</a>
-</div>

+ 0 - 20
docs/_themes/local/sidebarusefullinks.html

@@ -1,20 +0,0 @@
-<a href="https://github.com/borgbackup/borg"><img style="position: fixed; top: 0; right: 0; border: 0;"
-  src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub"></a>
-
-<h3>Useful Links</h3>
-<ul>
-  <li><a href="https://borgbackup.github.io/borgbackup/">Main Web Site</a></li>
-  <li><a href="https://pypi.python.org/pypi/borgbackup">PyPI packages</a></li>
-  <li><a href="https://github.com/borgbackup/borg/issues/214">Binaries</a></li>
-  <li><a href="https://github.com/borgbackup/borg/blob/master/CHANGES.rst">Current ChangeLog</a></li>
-  <li><a href="https://github.com/borgbackup/borg">GitHub</a></li>
-  <li><a href="https://github.com/borgbackup/borg/issues">Issue Tracker</a></li>
-  <li><a href="https://www.bountysource.com/teams/borgbackup">Bounties &amp; Fundraisers</a></li>
-  <li><a href="http://librelist.com/browser/borgbackup/">Mailing List</a></li>
-</ul>
-
-<h3>Related Projects</h3>
-<ul>
-  <li><a href="https://borgbackup.github.io/borgweb/">BorgWeb</a></li>
-</ul>
-

+ 0 - 173
docs/_themes/local/static/local.css_t

@@ -1,173 +0,0 @@
-@import url("basic.css");
-@import url(//fonts.googleapis.com/css?family=Black+Ops+One);
-
-body {
-  font-family: Arial, Helvetica, sans-serif;
-  background-color: black;
-  margin: 0;
-  padding: 0;
-  position: relative;
-}
-
-div.related {
-  display: none;
-  background-color: black;
-  padding: .4em;
-  width: 800px;
-  margin: 0 auto;
-}
-
-div.related a {
-  color: white;
-  text-decoration: none;
-}
-
-div.document {
-  width: 1030px;
-  margin: 0 auto;
-}
-
-div.documentwrapper {
-  float: right;
-  width: 760px;
-  padding: 0 20px 20px 20px;
-  color: #00cc00;
-  background-color: #000000;
-  margin-bottom: 2em;
-}
-
-div.sphinxsidebar {
-  margin-left: 0;
-  padding-right: 20px;
-  width: 230px;
-  background: #081008;
-  position: absolute;
-  top: 0;
-  min-height: 100%;
-}
-
-h1, h2, h3 {
-  font-weight: normal;
-  color: #33ff33;
-}
-
-h1 {
-  margin: .8em 0 .5em;
-  font-size: 200%;
-}
-
-h2 {
-  margin: 1.2em 0 .6em;
-  font-size: 140%;
-}
-
-h3 {
-  margin: 1.2em 0 .6em;
-  font-size: 110%;
-}
-
-ul {
-  padding-left: 1.2em;
-  margin-bottom: .3em;
-}
-
-ul ul {
-  font-size: 95%;
-}
-
-li {
-  margin: .1em 0;
-}
-
-a:link {
-  color: #dddd00;
-  text-decoration: none;
-}
-
-a:visited {
-  color: #990000;
-  text-decoration: none;
-}
-
-a:hover {
-  color: #dd0000;
-  border-bottom: 1px dotted #dd0000;
-}
-
-div.sphinxsidebar a:link, div.sphinxsidebar a:visited {
-  border-bottom: 1px dotted #555;
-}
-
-div.sphinxsidebar {
-  color: #00cc00;
-  background: 0000000;
-}
-
-div.sphinxsidebar input {
-  color: #00ff00;
-  background: 0000000;
-  border: 1px solid #444444;
-}
-
-pre {
-  padding: 10px 20px;
-  background: #101010;
-  color: #22cc22;
-  line-height: 1.5em;
-  border-bottom: 2px solid black;
-}
-
-pre a:link,
-pre a:visited {
-  color: #00b0e4;
-}
-
-div.sidebarlogo .title {
-  font-family: 'Black Ops One', cursive;
-  font-size: 500%;
-}
-
-div.sidebarlogo a {
-  color: #00dd00;
-}
-
-div.sidebarlogo .subtitle {
-  font-style: italic;
-  color: #777;
-}
-
-tt span.pre {
-  font-size: 110%;
-}
-
-dt {
-  font-size: 95%;
-}
-
-div.admonition p.admonition-title + p {
-  display: inline;
-}
-
-div.admonition p {
-  margin-bottom: 5px;
-}
-
-p.admonition-title {
-  display: inline;
-}
-
-p.admonition-title:after {
-  content: ":";
-}
-
-div.note {
-  background-color: #002211;
-  border-bottom: 2px solid #22dd22;
-}
-
-div.seealso {
-  background-color: #0fe;
-  border: 1px solid #0f6;
-  border-radius: .4em;
-  box-shadow: 2px 2px #dd6;
-}

+ 0 - 6
docs/_themes/local/theme.conf

@@ -1,6 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = local.css
-pygments_style = tango
-
-[options]

+ 11 - 0
docs/authors.rst

@@ -0,0 +1,11 @@
+.. include:: global.rst.inc
+
+.. include:: ../AUTHORS
+
+License
+=======
+
+.. _license:
+
+.. include:: ../LICENSE
+   :literal:

+ 575 - 3
docs/changes.rst

@@ -1,4 +1,576 @@
-.. include:: global.rst.inc
-.. _changelog:
+Changelog
+=========
 
 
-.. include:: ../CHANGES.rst
+Version 0.28.0
+--------------
+
+New features:
+
+- refactor return codes (exit codes), fixes #61
+- give a final status into the log output, including exit code, fixes #58
+- borg create backups atime and ctime additionally to mtime, fixes #317
+  - extract: support atime additionally to mtime
+  - FUSE: support ctime and atime additionally to mtime
+- support borg --version
+
+Bug fixes:
+
+- setup.py: fix bug related to BORG_LZ4_PREFIX processing
+- borg mount: fix unlocking of repository at umount time, fixes #331
+- fix reading files without touching their atime, #334
+- non-ascii ACL fixes for Linux, FreeBSD and OS X, #277
+- fix acl_use_local_uid_gid() and add a test for it, attic #359
+- borg upgrade: do not upgrade repositories in place by default, #299
+- fix cascading failure with the index conversion code, #269
+- borg check: implement 'cmdline' archive metadata value decoding, #311
+
+Other changes:
+
+- improve file size displays
+- convert to more flexible size formatters
+- explicitely commit to the units standard, #289
+- archiver: add E status (means that an error occured when processing this
+  (single) item
+- do binary releases via "github releases", closes #214
+- create: use -x and --one-file-system (was: --do-not-cross-mountpoints), #296
+- a lot of changes related to using "logging" module and screen output, #233
+- show progress display if on a tty, output more progress information, #303
+- factor out status output so it is consistent, fix surrogates removal,
+  maybe fixes #309
+- benchmarks: test create, extract, list, delete, info, check, help, fixes #146
+- benchmarks: test with both the binary and the python code
+- tests:
+
+  - travis: also run tests on Python 3.5
+  - travis: use tox -r so it rebuilds the tox environments
+  - test the generated pyinstaller-based binary by archiver unit tests, #215
+  - vagrant: tests: announce whether fakeroot is used or not
+  - vagrant: add vagrant user to fuse group for debianoid systems also
+  - vagrant: llfuse install on darwin needs pkgconfig installed
+  - archiver tests: test with both the binary and the python code, fixes #215
+- docs:
+
+  - moved docs to borgbackup.readthedocs.org, #155
+  - a lot of fixes and improvements, use mobile-friendly RTD standard theme
+  - use zlib,6 compression in some examples, fixes #275
+  - add missing rename usage to docs, closes #279
+  - include the help offered by borg help <topic> in the usage docs, fixes #293
+  - include a list of major changes compared to attic into README, fixes #224
+  - add OS X install instructions, #197
+  - more details about the release process, #260
+  - fix linux glibc requirement (binaries built on debian7 now)
+  - build: move usage and API generation to setup.py
+  - update docs about return codes, #61
+  - remove api docs (too much breakage on rtd)
+  - borgbackup install + basics presentation (asciinema)
+  - describe the current style guide in documentation
+    
+
+Version 0.27.0
+--------------
+
+New features:
+
+- "borg upgrade" command - attic -> borg one time converter / migration, #21
+- temporary hack to avoid using lots of disk space for chunks.archive.d, #235:
+  To use it: rm -rf chunks.archive.d ; touch chunks.archive.d
+- respect XDG_CACHE_HOME, attic #181
+- add support for arbitrary SSH commands, attic #99
+- borg delete --cache-only REPO (only delete cache, not REPO), attic #123
+
+
+Bug fixes:
+
+- use Debian 7 (wheezy) to build pyinstaller borgbackup binaries, fixes slow
+  down observed when running the Centos6-built binary on Ubuntu, #222
+- do not crash on empty lock.roster, fixes #232
+- fix multiple issues with the cache config version check, #234
+- fix segment entry header size check, attic #352
+  plus other error handling improvements / code deduplication there.
+- always give segment and offset in repo IntegrityErrors
+
+
+Other changes:
+
+- stop producing binary wheels, remove docs about it, #147
+- docs:
+  - add warning about prune
+  - generate usage include files only as needed
+  - development docs: add Vagrant section
+  - update / improve / reformat FAQ
+  - hint to single-file pyinstaller binaries from README
+
+
+Version 0.26.1
+--------------
+
+This is a minor update, just docs and new pyinstaller binaries.
+
+- docs update about python and binary requirements
+- better docs for --read-special, fix #220
+- re-built the binaries, fix #218 and #213 (glibc version issue)
+- update web site about single-file pyinstaller binaries
+
+Note: if you did a python-based installation, there is no need to upgrade.
+
+
+Version 0.26.0
+--------------
+
+New features:
+
+- Faster cache sync (do all in one pass, remove tar/compression stuff), #163
+- BORG_REPO env var to specify the default repo, #168
+- read special files as if they were regular files, #79
+- implement borg create --dry-run, attic issue #267
+- Normalize paths before pattern matching on OS X, #143
+- support OpenBSD and NetBSD (except xattrs/ACLs)
+- support / run tests on Python 3.5
+
+Bug fixes:
+
+- borg mount repo: use absolute path, attic #200, attic #137
+- chunker: use off_t to get 64bit on 32bit platform, #178
+- initialize chunker fd to -1, so it's not equal to STDIN_FILENO (0)
+- fix reaction to "no" answer at delete repo prompt, #182
+- setup.py: detect lz4.h header file location
+- to support python < 3.2.4, add less buggy argparse lib from 3.2.6 (#194)
+- fix for obtaining ``char *`` from temporary Python value (old code causes
+  a compile error on Mint 17.2)
+- llfuse 0.41 install troubles on some platforms, require < 0.41
+  (UnicodeDecodeError exception due to non-ascii llfuse setup.py)
+- cython code: add some int types to get rid of unspecific python add /
+  subtract operations (avoid ``undefined symbol FPE_``... error on some platforms)
+- fix verbose mode display of stdin backup
+- extract: warn if a include pattern never matched, fixes #209,
+  implement counters for Include/ExcludePatterns
+- archive names with slashes are invalid, attic issue #180
+- chunker: add a check whether the POSIX_FADV_DONTNEED constant is defined -
+  fixes building on OpenBSD.
+
+Other changes:
+
+- detect inconsistency / corruption / hash collision, #170
+- replace versioneer with setuptools_scm, #106
+- docs:
+
+  - pkg-config is needed for llfuse installation
+  - be more clear about pruning, attic issue #132
+- unit tests:
+
+  - xattr: ignore security.selinux attribute showing up
+  - ext3 seems to need a bit more space for a sparse file
+  - do not test lzma level 9 compression (avoid MemoryError)
+  - work around strange mtime granularity issue on netbsd, fixes #204
+  - ignore st_rdev if file is not a block/char device, fixes #203
+  - stay away from the setgid and sticky mode bits
+- use Vagrant to do easy cross-platform testing (#196), currently:
+
+  - Debian 7 "wheezy" 32bit, Debian 8 "jessie" 64bit
+  - Ubuntu 12.04 32bit, Ubuntu 14.04 64bit
+  - Centos 7 64bit
+  - FreeBSD 10.2 64bit
+  - OpenBSD 5.7 64bit
+  - NetBSD 6.1.5 64bit
+  - Darwin (OS X Yosemite)
+
+
+Version 0.25.0
+--------------
+
+Compatibility notes:
+
+- lz4 compression library (liblz4) is a new requirement (#156)
+- the new compression code is very compatible: as long as you stay with zlib
+  compression, older borg releases will still be able to read data from a
+  repo/archive made with the new code (note: this is not the case for the
+  default "none" compression, use "zlib,0" if you want a "no compression" mode
+  that can be read by older borg). Also the new code is able to read repos and
+  archives made with older borg versions (for all zlib levels  0..9).
+
+Deprecations:
+
+- --compression N (with N being a number, as in 0.24) is deprecated.
+  We keep the --compression 0..9 for now to not break scripts, but it is
+  deprecated and will be removed later, so better fix your scripts now:
+  --compression 0 (as in 0.24) is the same as --compression zlib,0 (now).
+  BUT: if you do not want compression, you rather want --compression none
+  (which is the default).
+  --compression 1 (in 0.24) is the same as --compression zlib,1 (now)
+  --compression 9 (in 0.24) is the same as --compression zlib,9 (now)
+
+New features:
+
+- create --compression none (default, means: do not compress, just pass through
+  data "as is". this is more efficient than zlib level 0 as used in borg 0.24)
+- create --compression lz4 (super-fast, but not very high compression)
+- create --compression zlib,N (slower, higher compression, default for N is 6)
+- create --compression lzma,N (slowest, highest compression, default N is 6)
+- honor the nodump flag (UF_NODUMP) and do not backup such items
+- list --short just outputs a simple list of the files/directories in an archive
+
+Bug fixes:
+
+- fixed --chunker-params parameter order confusion / malfunction, fixes #154
+- close fds of segments we delete (during compaction)
+- close files which fell out the lrucache
+- fadvise DONTNEED now is only called for the byte range actually read, not for
+  the whole file, fixes #158.
+- fix issue with negative "all archives" size, fixes #165
+- restore_xattrs: ignore if setxattr fails with EACCES, fixes #162
+
+Other changes:
+
+- remove fakeroot requirement for tests, tests run faster without fakeroot
+  (test setup does not fail any more without fakeroot, so you can run with or
+  without fakeroot), fixes #151 and #91.
+- more tests for archiver
+- recover_segment(): don't assume we have an fd for segment
+- lrucache refactoring / cleanup, add dispose function, py.test tests
+- generalize hashindex code for any key length (less hardcoding)
+- lock roster: catch file not found in remove() method and ignore it
+- travis CI: use requirements file
+- improved docs:
+
+  - replace hack for llfuse with proper solution (install libfuse-dev)
+  - update docs about compression
+  - update development docs about fakeroot
+  - internals: add some words about lock files / locking system
+  - support: mention BountySource and for what it can be used
+  - theme: use a lighter green
+  - add pypi, wheel, dist package based install docs
+  - split install docs into system-specific preparations and generic instructions
+
+
+Version 0.24.0
+--------------
+
+Incompatible changes (compared to 0.23):
+
+- borg now always issues --umask NNN option when invoking another borg via ssh
+  on the repository server. By that, it's making sure it uses the same umask
+  for remote repos as for local ones. Because of this, you must upgrade both
+  server and client(s) to 0.24.
+- the default umask is 077 now (if you do not specify via --umask) which might
+  be a different one as you used previously. The default umask avoids that
+  you accidentally give access permissions for group and/or others to files
+  created by borg (e.g. the repository).
+
+Deprecations:
+
+- "--encryption passphrase" mode is deprecated, see #85 and #97.
+  See the new "--encryption repokey" mode for a replacement.
+
+New features:
+
+- borg create --chunker-params ... to configure the chunker, fixes #16
+  (attic #302, attic #300, and somehow also #41).
+  This can be used to reduce memory usage caused by chunk management overhead,
+  so borg does not create a huge chunks index/repo index and eats all your RAM
+  if you back up lots of data in huge files (like VM disk images).
+  See docs/misc/create_chunker-params.txt for more information.
+- borg info now reports chunk counts in the chunk index.
+- borg create --compression 0..9 to select zlib compression level, fixes #66
+  (attic #295).
+- borg init --encryption repokey (to store the encryption key into the repo),
+  fixes #85
+- improve at-end error logging, always log exceptions and set exit_code=1
+- LoggedIO: better error checks / exceptions / exception handling
+- implement --remote-path to allow non-default-path borg locations, #125
+- implement --umask M and use 077 as default umask for better security, #117
+- borg check: give a named single archive to it, fixes #139
+- cache sync: show progress indication
+- cache sync: reimplement the chunk index merging in C
+
+Bug fixes:
+
+- fix segfault that happened for unreadable files (chunker: n needs to be a
+  signed size_t), #116
+- fix the repair mode, #144
+- repo delete: add destroy to allowed rpc methods, fixes issue #114
+- more compatible repository locking code (based on mkdir), maybe fixes #92
+  (attic #317, attic #201).
+- better Exception msg if no Borg is installed on the remote repo server, #56
+- create a RepositoryCache implementation that can cope with >2GiB,
+  fixes attic #326.
+- fix Traceback when running check --repair, attic #232
+- clarify help text, fixes #73.
+- add help string for --no-files-cache, fixes #140
+
+Other changes:
+
+- improved docs:
+
+  - added docs/misc directory for misc. writeups that won't be included
+    "as is" into the html docs.
+  - document environment variables and return codes (attic #324, attic #52)
+  - web site: add related projects, fix web site url, IRC #borgbackup
+  - Fedora/Fedora-based install instructions added to docs
+  - Cygwin-based install instructions added to docs
+  - updated AUTHORS
+  - add FAQ entries about redundancy / integrity
+  - clarify that borg extract uses the cwd as extraction target
+  - update internals doc about chunker params, memory usage and compression
+  - added docs about development
+  - add some words about resource usage in general
+  - document how to backup a raw disk
+  - add note about how to run borg from virtual env
+  - add solutions for (ll)fuse installation problems
+  - document what borg check does, fixes #138
+  - reorganize borgbackup.github.io sidebar, prev/next at top
+  - deduplicate and refactor the docs / README.rst
+
+- use borg-tmp as prefix for temporary files / directories
+- short prune options without "keep-" are deprecated, do not suggest them
+- improved tox configuration
+- remove usage of unittest.mock, always use mock from pypi
+- use entrypoints instead of scripts, for better use of the wheel format and
+  modern installs
+- add requirements.d/development.txt and modify tox.ini
+- use travis-ci for testing based on Linux and (new) OS X
+- use coverage.py, pytest-cov and codecov.io for test coverage support
+
+I forgot to list some stuff already implemented in 0.23.0, here they are:
+
+New features:
+
+- efficient archive list from manifest, meaning a big speedup for slow
+  repo connections and "list <repo>", "delete <repo>", "prune" (attic #242,
+  attic #167)
+- big speedup for chunks cache sync (esp. for slow repo connections), fixes #18
+- hashindex: improve error messages
+
+Other changes:
+
+- explicitly specify binary mode to open binary files
+- some easy micro optimizations
+
+
+Version 0.23.0
+--------------
+
+Incompatible changes (compared to attic, fork related):
+
+- changed sw name and cli command to "borg", updated docs
+- package name (and name in urls) uses "borgbackup" to have less collisions
+- changed repo / cache internal magic strings from ATTIC* to BORG*,
+  changed cache location to .cache/borg/ - this means that it currently won't
+  accept attic repos (see issue #21 about improving that)
+
+Bug fixes:
+
+- avoid defect python-msgpack releases, fixes attic #171, fixes attic #185
+- fix traceback when trying to do unsupported passphrase change, fixes attic #189
+- datetime does not like the year 10.000, fixes attic #139
+- fix "info" all archives stats, fixes attic #183
+- fix parsing with missing microseconds, fixes attic #282
+- fix misleading hint the fuse ImportError handler gave, fixes attic #237
+- check unpacked data from RPC for tuple type and correct length, fixes attic #127
+- fix Repository._active_txn state when lock upgrade fails
+- give specific path to xattr.is_enabled(), disable symlink setattr call that
+  always fails
+- fix test setup for 32bit platforms, partial fix for attic #196
+- upgraded versioneer, PEP440 compliance, fixes attic #257
+
+New features:
+
+- less memory usage: add global option --no-cache-files
+- check --last N (only check the last N archives)
+- check: sort archives in reverse time order
+- rename repo::oldname newname (rename repository)
+- create -v output more informative
+- create --progress (backup progress indicator)
+- create --timestamp (utc string or reference file/dir)
+- create: if "-" is given as path, read binary from stdin
+- extract: if --stdout is given, write all extracted binary data to stdout
+- extract --sparse (simple sparse file support)
+- extra debug information for 'fread failed'
+- delete <repo> (deletes whole repo + local cache)
+- FUSE: reflect deduplication in allocated blocks
+- only allow whitelisted RPC calls in server mode
+- normalize source/exclude paths before matching
+- use posix_fadvise to not spoil the OS cache, fixes attic #252
+- toplevel error handler: show tracebacks for better error analysis
+- sigusr1 / sigint handler to print current file infos - attic PR #286
+- RPCError: include the exception args we get from remote
+
+Other changes:
+
+- source: misc. cleanups, pep8, style
+- docs and faq improvements, fixes, updates
+- cleanup crypto.pyx, make it easier to adapt to other AES modes
+- do os.fsync like recommended in the python docs
+- source: Let chunker optionally work with os-level file descriptor.
+- source: Linux: remove duplicate os.fsencode calls
+- source: refactor _open_rb code a bit, so it is more consistent / regular
+- source: refactor indicator (status) and item processing
+- source: use py.test for better testing, flake8 for code style checks
+- source: fix tox >=2.0 compatibility (test runner)
+- pypi package: add python version classifiers, add FreeBSD to platforms
+
+
+Attic Changelog
+---------------
+
+Here you can see the full list of changes between each Attic release until Borg
+forked from Attic:
+
+Version 0.17
+~~~~~~~~~~~~
+
+(bugfix release, released on X)
+- Fix hashindex ARM memory alignment issue (#309)
+- Improve hashindex error messages (#298)
+
+Version 0.16
+~~~~~~~~~~~~
+
+(bugfix release, released on May 16, 2015)
+- Fix typo preventing the security confirmation prompt from working (#303)
+- Improve handling of systems with improperly configured file system encoding (#289)
+- Fix "All archives" output for attic info. (#183)
+- More user friendly error message when repository key file is not found (#236)
+- Fix parsing of iso 8601 timestamps with zero microseconds (#282)
+
+Version 0.15
+~~~~~~~~~~~~
+
+(bugfix release, released on Apr 15, 2015)
+- xattr: Be less strict about unknown/unsupported platforms (#239)
+- Reduce repository listing memory usage (#163).
+- Fix BrokenPipeError for remote repositories (#233)
+- Fix incorrect behavior with two character directory names (#265, #268)
+- Require approval before accessing relocated/moved repository (#271)
+- Require approval before accessing previously unknown unencrypted repositories (#271)
+- Fix issue with hash index files larger than 2GB.
+- Fix Python 3.2 compatibility issue with noatime open() (#164)
+- Include missing pyx files in dist files (#168)
+
+Version 0.14
+~~~~~~~~~~~~
+
+(feature release, released on Dec 17, 2014)
+
+- Added support for stripping leading path segments (#95)
+  "attic extract --strip-segments X"
+- Add workaround for old Linux systems without acl_extended_file_no_follow (#96)
+- Add MacPorts' path to the default openssl search path (#101)
+- HashIndex improvements, eliminates unnecessary IO on low memory systems.
+- Fix "Number of files" output for attic info. (#124)
+- limit create file permissions so files aren't read while restoring
+- Fix issue with empty xattr values (#106)
+
+Version 0.13
+~~~~~~~~~~~~
+
+(feature release, released on Jun 29, 2014)
+
+- Fix sporadic "Resource temporarily unavailable" when using remote repositories
+- Reduce file cache memory usage (#90)
+- Faster AES encryption (utilizing AES-NI when available)
+- Experimental Linux, OS X and FreeBSD ACL support (#66)
+- Added support for backup and restore of BSDFlags (OSX, FreeBSD) (#56)
+- Fix bug where xattrs on symlinks were not correctly restored
+- Added cachedir support. CACHEDIR.TAG compatible cache directories
+  can now be excluded using ``--exclude-caches`` (#74)
+- Fix crash on extreme mtime timestamps (year 2400+) (#81)
+- Fix Python 3.2 specific lockf issue (EDEADLK)
+
+Version 0.12
+~~~~~~~~~~~~
+
+(feature release, released on April 7, 2014)
+
+- Python 3.4 support (#62)
+- Various documentation improvements a new style
+- ``attic mount`` now supports mounting an entire repository not only
+  individual archives (#59)
+- Added option to restrict remote repository access to specific path(s):
+  ``attic serve --restrict-to-path X`` (#51)
+- Include "all archives" size information in "--stats" output. (#54)
+- Added ``--stats`` option to ``attic delete`` and ``attic prune``
+- Fixed bug where ``attic prune`` used UTC instead of the local time zone
+  when determining which archives to keep.
+- Switch to SI units (Power of 1000 instead 1024) when printing file sizes
+
+Version 0.11
+~~~~~~~~~~~~
+
+(feature release, released on March 7, 2014)
+
+- New "check" command for repository consistency checking (#24)
+- Documentation improvements
+- Fix exception during "attic create" with repeated files (#39)
+- New "--exclude-from" option for attic create/extract/verify.
+- Improved archive metadata deduplication.
+- "attic verify" has been deprecated. Use "attic extract --dry-run" instead.
+- "attic prune --hourly|daily|..." has been deprecated.
+  Use "attic prune --keep-hourly|daily|..." instead.
+- Ignore xattr errors during "extract" if not supported by the filesystem. (#46)
+
+Version 0.10
+~~~~~~~~~~~~
+
+(bugfix release, released on Jan 30, 2014)
+
+- Fix deadlock when extracting 0 sized files from remote repositories
+- "--exclude" wildcard patterns are now properly applied to the full path
+  not just the file name part (#5).
+- Make source code endianness agnostic (#1)
+
+Version 0.9
+~~~~~~~~~~~
+
+(feature release, released on Jan 23, 2014)
+
+- Remote repository speed and reliability improvements.
+- Fix sorting of segment names to ignore NFS left over files. (#17)
+- Fix incorrect display of time (#13)
+- Improved error handling / reporting. (#12)
+- Use fcntl() instead of flock() when locking repository/cache. (#15)
+- Let ssh figure out port/user if not specified so we don't override .ssh/config (#9)
+- Improved libcrypto path detection (#23).
+
+Version 0.8.1
+~~~~~~~~~~~~~
+
+(bugfix release, released on Oct 4, 2013)
+
+- Fix segmentation fault issue.
+
+Version 0.8
+~~~~~~~~~~~
+
+(feature release, released on Oct 3, 2013)
+
+- Fix xattr issue when backing up sshfs filesystems (#4)
+- Fix issue with excessive index file size (#6)
+- Support access of read only repositories.
+- New syntax to enable repository encryption:
+    attic init --encryption="none|passphrase|keyfile".
+- Detect and abort if repository is older than the cache.
+
+
+Version 0.7
+~~~~~~~~~~~
+
+(feature release, released on Aug 5, 2013)
+
+- Ported to FreeBSD
+- Improved documentation
+- Experimental: Archives mountable as fuse filesystems.
+- The "user." prefix is no longer stripped from xattrs on Linux
+
+
+Version 0.6.1
+~~~~~~~~~~~~~
+
+(bugfix release, released on July 19, 2013)
+
+- Fixed an issue where mtime was not always correctly restored.
+
+
+Version 0.6
+~~~~~~~~~~~
+
+First public release on July 9, 2013

+ 10 - 4
docs/conf.py

@@ -19,6 +19,8 @@ sys.path.insert(0, os.path.abspath('..'))
 
 
 from borg import __version__ as sw_version
 from borg import __version__ as sw_version
 
 
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
 # -- General configuration -----------------------------------------------------
 # -- General configuration -----------------------------------------------------
 
 
 # If your documentation needs a minimal Sphinx version, state it here.
 # If your documentation needs a minimal Sphinx version, state it here.
@@ -92,7 +94,11 @@ pygments_style = 'sphinx'
 
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 # a list of builtin themes.
-html_theme = 'local'
+#html_theme = ''
+if not on_rtd:  # only import and set the theme if we're building docs locally
+    import sphinx_rtd_theme
+    html_theme = 'sphinx_rtd_theme'
+    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # further.  For a list of options available for each theme, see the
@@ -111,17 +117,17 @@ html_theme_path = ['_themes']
 
 
 # The name of an image file (relative to this directory) to place at the top
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
 # of the sidebar.
-#html_logo = None
+html_logo = '_static/favicon.ico'
 
 
 # The name of an image file (within the static path) to use as favicon of the
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
 # pixels large.
-html_favicon = 'favicon.ico'
+html_favicon = '_static/favicon.ico'
 
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = []
+#html_static_path = ['_static']
 
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
 # using the given strftime format.

+ 48 - 31
docs/development.rst

@@ -9,6 +9,15 @@ This chapter will get you started with |project_name|' development.
 |project_name| is written in Python (with a little bit of Cython and C for
 |project_name| is written in Python (with a little bit of Cython and C for
 the performance critical parts).
 the performance critical parts).
 
 
+Style guide
+-----------
+
+We generally follow `pep8
+<https://www.python.org/dev/peps/pep-0008/>`_, with 120 columns
+instead of 79. We do *not* use form-feed (``^L``) characters to
+separate sections either. The `flake8
+<https://flake8.readthedocs.org/>`_ commandline tool should be used to
+check for style errors before sending pull requests.
 
 
 Building a development environment
 Building a development environment
 ----------------------------------
 ----------------------------------
@@ -68,6 +77,9 @@ Now run::
 
 
 Then point a web browser at docs/_build/html/index.html.
 Then point a web browser at docs/_build/html/index.html.
 
 
+The website is updated automatically through Github web hooks on the
+main repository.
+
 Using Vagrant
 Using Vagrant
 -------------
 -------------
 
 
@@ -91,49 +103,54 @@ Usage::
      vagrant scp OS:/vagrant/borg/borg/dist/borg .
      vagrant scp OS:/vagrant/borg/borg/dist/borg .
 
 
 
 
-Creating a new release
-----------------------
+Creating standalone binaries
+----------------------------
 
 
-Checklist::
+Make sure you have everything built and installed (including llfuse and fuse).
+When using the Vagrant VMs, pyinstaller will already be installed.
 
 
-- all issues for this milestone closed?
-- any low hanging fruit left on the issue tracker?
-- run tox on all supported platforms via vagrant, check for test fails.
-- is Travis CI happy also?
-- update CHANGES.rst (compare to git log). check version number of upcoming release.
-- check MANIFEST.in and setup.py - are they complete?
-- tag the release::
+With virtual env activated::
 
 
-  git tag -s -m "tagged release" 0.26.0
+  pip install pyinstaller>=3.0  # or git checkout master
+  pyinstaller -F -n borg-PLATFORM --hidden-import=logging.config borg/__main__.py
+  for file in dist/borg-*; do gpg --armor --detach-sign $file; done
 
 
-- cd docs ; make html  # to update the usage include files
-- update website with the html
-- create a release on PyPi::
+If you encounter issues, see also our `Vagrantfile` for details.
 
 
-    python setup.py register sdist upload --identity="Thomas Waldmann" --sign
+.. note:: Standalone binaries built with pyinstaller are supposed to
+          work on same OS, same architecture (x86 32bit, amd64 64bit)
+          without external dependencies.
 
 
-- close release milestone.
-- announce on::
 
 
-  - mailing list
-  - Twitter
-  - IRC channel (topic)
+Creating a new release
+----------------------
 
 
-- create standalone binaries and link them from issue tracker: https://github.com/borgbackup/borg/issues/214
+Checklist:
 
 
+- make sure all issues for this milestone are closed or moved to the
+  next milestone
+- find and fix any low hanging fruit left on the issue tracker
+- run tox on all supported platforms via vagrant, check for test failures
+- check that Travis CI is also happy
+- update ``CHANGES.rst``, based on ``git log $PREVIOUS_RELEASE..``
+- check version number of upcoming release in ``CHANGES.rst``
+- verify that ``MANIFEST.in`` and ``setup.py`` are complete
+- tag the release::
 
 
-Creating standalone binaries
-----------------------------
+    git tag -s -m "tagged/signed release X.Y.Z" X.Y.Z
 
 
-Make sure you have everything built and installed (including llfuse and fuse).
+- build fresh docs and update the web site with them
+- create a release on PyPi::
 
 
-With virtual env activated::
+    python setup.py register sdist upload --identity="Thomas Waldmann" --sign
 
 
-  pip install pyinstaller>=3.0  # or git checkout master
-  pyinstaller -F -n borg-PLATFORM --hidden-import=logging.config borg/__main__.py
-  ls -l dist/*
+- close release milestone on Github
+- announce on:
 
 
-If you encounter issues, see also our `Vagrantfile` for details.
+ - `mailing list <mailto:borgbackup@librelist.org>`_
+ - Twitter (follow @ThomasJWaldmann for these tweets)
+ - `IRC channel <irc://irc.freenode.net/borgbackup>`_ (change ``/topic``)
 
 
-Note: Standalone binaries built with pyinstaller are supposed to work on same OS,
-      same architecture (x86 32bit, amd64 64bit) without external dependencies.
+- create a Github release, include:
+  * standalone binaries (see above for how to create them)
+  * a link to ``CHANGES.rst``

+ 130 - 75
docs/faq.rst

@@ -5,24 +5,30 @@ Frequently asked questions
 ==========================
 ==========================
 
 
 Can I backup VM disk images?
 Can I backup VM disk images?
-    Yes, the :ref:`deduplication <deduplication_def>` technique used by
-    |project_name| makes sure only the modified parts of the file are stored.
-    Also, we have optional simple sparse file support for extract.
+----------------------------
+
+Yes, the `deduplication`_ technique used by
+|project_name| makes sure only the modified parts of the file are stored.
+Also, we have optional simple sparse file support for extract.
 
 
 Can I backup from multiple servers into a single repository?
 Can I backup from multiple servers into a single repository?
-    Yes, but in order for the deduplication used by |project_name| to work, it
-    needs to keep a local cache containing checksums of all file
-    chunks already stored in the repository. This cache is stored in
-    ``~/.cache/borg/``.  If |project_name| detects that a repository has been
-    modified since the local cache was updated it will need to rebuild
-    the cache. This rebuild can be quite time consuming.
-
-    So, yes it's possible. But it will be most efficient if a single
-    repository is only modified from one place. Also keep in mind that
-    |project_name| will keep an exclusive lock on the repository while creating
-    or deleting archives, which may make *simultaneous* backups fail.
+------------------------------------------------------------
+
+Yes, but in order for the deduplication used by |project_name| to work, it
+needs to keep a local cache containing checksums of all file
+chunks already stored in the repository. This cache is stored in
+``~/.cache/borg/``.  If |project_name| detects that a repository has been
+modified since the local cache was updated it will need to rebuild
+the cache. This rebuild can be quite time consuming.
+
+So, yes it's possible. But it will be most efficient if a single
+repository is only modified from one place. Also keep in mind that
+|project_name| will keep an exclusive lock on the repository while creating
+or deleting archives, which may make *simultaneous* backups fail.
 
 
 Which file types, attributes, etc. are preserved?
 Which file types, attributes, etc. are preserved?
+-------------------------------------------------
+
     * Directories
     * Directories
     * Regular files
     * Regular files
     * Hardlinks (considering all files in the same archive)
     * Hardlinks (considering all files in the same archive)
@@ -40,6 +46,8 @@ Which file types, attributes, etc. are preserved?
     * BSD flags on OS X and FreeBSD
     * BSD flags on OS X and FreeBSD
 
 
 Which file types, attributes, etc. are *not* preserved?
 Which file types, attributes, etc. are *not* preserved?
+-------------------------------------------------------
+
     * UNIX domain sockets (because it does not make sense - they are
     * UNIX domain sockets (because it does not make sense - they are
       meaningless without the running process that created them and the process
       meaningless without the running process that created them and the process
       needs to recreate them in any case). So, don't panic if your backup
       needs to recreate them in any case). So, don't panic if your backup
@@ -50,91 +58,138 @@ Which file types, attributes, etc. are *not* preserved?
       Archive extraction has optional support to extract all-zero chunks as
       Archive extraction has optional support to extract all-zero chunks as
       holes in a sparse file.
       holes in a sparse file.
 
 
+Why is my backup bigger than with attic? Why doesn't |project_name| do compression by default?
+----------------------------------------------------------------------------------------------
+
+Attic was rather unflexible when it comes to compression, it always
+compressed using zlib level 6 (no way to switch compression off or
+adjust the level or algorithm).
+
+|project_name| offers a lot of different compression algorithms and
+levels. Which of them is the best for you pretty much depends on your
+use case, your data, your hardware - so you need to do an informed
+decision about whether you want to use compression, which algorithm
+and which level you want to use. This is why compression defaults to
+none.
+
 How can I specify the encryption passphrase programmatically?
 How can I specify the encryption passphrase programmatically?
-    The encryption passphrase can be specified programmatically using the
-    `BORG_PASSPHRASE` environment variable. This is convenient when setting up
-    automated encrypted backups. Another option is to use
-    key file based encryption with a blank passphrase. See
-    :ref:`encrypted_repos` for more details.
+-------------------------------------------------------------
+      
+The encryption passphrase can be specified programmatically using the
+`BORG_PASSPHRASE` environment variable. This is convenient when setting up
+automated encrypted backups. Another option is to use
+key file based encryption with a blank passphrase. See
+:ref:`encrypted_repos` for more details.
+
+.. _password_env:
+.. note:: Be careful how you set the environment; using the ``env``
+          command, a ``system()`` call or using inline shell scripts
+          might expose the credentials in the process list directly
+          and they will be readable to all users on a system. Using
+          ``export`` in a shell script file should be safe, however, as
+          the environment of a process is `accessible only to that
+          user
+          <http://security.stackexchange.com/questions/14000/environment-variable-accessibility-in-linux/14009#14009>`_.
 
 
 When backing up to remote encrypted repos, is encryption done locally?
 When backing up to remote encrypted repos, is encryption done locally?
-    Yes, file and directory metadata and data is locally encrypted, before
-    leaving the local machine. We do not mean the transport layer encryption
-    by that, but the data/metadata itself. Transport layer encryption (e.g.
-    when ssh is used as a transport) applies additionally.
+----------------------------------------------------------------------
+     
+Yes, file and directory metadata and data is locally encrypted, before
+leaving the local machine. We do not mean the transport layer encryption
+by that, but the data/metadata itself. Transport layer encryption (e.g.
+when ssh is used as a transport) applies additionally.
 
 
 When backing up to remote servers, do I have to trust the remote server?
 When backing up to remote servers, do I have to trust the remote server?
-    Yes and No.
-    No, as far as data confidentiality is concerned - if you use encryption,
-    all your files/dirs data and metadata are stored in their encrypted form
-    into the repository.
-    Yes, as an attacker with access to the remote server could delete (or
-    otherwise make unavailable) all your backups.
+------------------------------------------------------------------------
+
+Yes and No.
+
+No, as far as data confidentiality is concerned - if you use encryption,
+all your files/dirs data and metadata are stored in their encrypted form
+into the repository.
+
+Yes, as an attacker with access to the remote server could delete (or
+otherwise make unavailable) all your backups.
 
 
 If a backup stops mid-way, does the already-backed-up data stay there?
 If a backup stops mid-way, does the already-backed-up data stay there?
-    Yes, |project_name| supports resuming backups.
-    During a backup a special checkpoint archive named ``<archive-name>.checkpoint``
-    is saved every checkpoint interval (the default value for this is 5
-    minutes) containing all the data backed-up until that point. This means
-    that at most <checkpoint interval> worth of data needs to be retransmitted
-    if a backup needs to be restarted.
-    Once your backup has finished successfully, you can delete all ``*.checkpoint``
-    archives.
+----------------------------------------------------------------------
+
+Yes, |project_name| supports resuming backups.
+
+During a backup a special checkpoint archive named ``<archive-name>.checkpoint``
+is saved every checkpoint interval (the default value for this is 5
+minutes) containing all the data backed-up until that point. This means
+that at most <checkpoint interval> worth of data needs to be retransmitted
+if a backup needs to be restarted.
+
+Once your backup has finished successfully, you can delete all ``*.checkpoint``
+archives.
 
 
 If it crashes with a UnicodeError, what can I do?
 If it crashes with a UnicodeError, what can I do?
-    Check if your encoding is set correctly. For most POSIX-like systems, try::
+-------------------------------------------------
+
+Check if your encoding is set correctly. For most POSIX-like systems, try::
 
 
-        export LANG=en_US.UTF-8  # or similar, important is correct charset
+  export LANG=en_US.UTF-8  # or similar, important is correct charset
 
 
 I can't extract non-ascii filenames by giving them on the commandline!?
 I can't extract non-ascii filenames by giving them on the commandline!?
-    This might be due to different ways to represent some characters in unicode
-    or due to other non-ascii encoding issues.
-    If you run into that, try this:
+-----------------------------------------------------------------------
 
 
-    - avoid the non-ascii characters on the commandline by e.g. extracting
-      the parent directory (or even everything)
-    - mount the repo using FUSE and use some file manager
+This might be due to different ways to represent some characters in unicode
+or due to other non-ascii encoding issues.
+
+If you run into that, try this:
+
+- avoid the non-ascii characters on the commandline by e.g. extracting
+  the parent directory (or even everything)
+- mount the repo using FUSE and use some file manager
 
 
 Can |project_name| add redundancy to the backup data to deal with hardware malfunction?
 Can |project_name| add redundancy to the backup data to deal with hardware malfunction?
-    No, it can't. While that at first sounds like a good idea to defend against
-    some defect HDD sectors or SSD flash blocks, dealing with this in a
-    reliable way needs a lot of low-level storage layout information and
-    control which we do not have (and also can't get, even if we wanted).
+---------------------------------------------------------------------------------------
+
+No, it can't. While that at first sounds like a good idea to defend against
+some defect HDD sectors or SSD flash blocks, dealing with this in a
+reliable way needs a lot of low-level storage layout information and
+control which we do not have (and also can't get, even if we wanted).
 
 
-    So, if you need that, consider RAID or a filesystem that offers redundant
-    storage or just make backups to different locations / different hardware.
+So, if you need that, consider RAID or a filesystem that offers redundant
+storage or just make backups to different locations / different hardware.
 
 
-    See also `ticket 225 <https://github.com/borgbackup/borg/issues/225>`_.
+See also `ticket 225 <https://github.com/borgbackup/borg/issues/225>`_.
 
 
 Can |project_name| verify data integrity of a backup archive?
 Can |project_name| verify data integrity of a backup archive?
-    Yes, if you want to detect accidental data damage (like bit rot), use the
-    ``check`` operation. It will notice corruption using CRCs and hashes.
-    If you want to be able to detect malicious tampering also, use a encrypted
-    repo. It will then be able to check using CRCs and HMACs.
+-------------------------------------------------------------
+
+Yes, if you want to detect accidental data damage (like bit rot), use the
+``check`` operation. It will notice corruption using CRCs and hashes.
+If you want to be able to detect malicious tampering also, use a encrypted
+repo. It will then be able to check using CRCs and HMACs.
 
 
 Why was Borg forked from Attic?
 Why was Borg forked from Attic?
-    Borg was created in May 2015 in response to the difficulty of getting new
-    code or larger changes incorporated into Attic and establishing a bigger
-    developer community / more open development.
+-------------------------------
+
+Borg was created in May 2015 in response to the difficulty of getting new
+code or larger changes incorporated into Attic and establishing a bigger
+developer community / more open development.
 
 
-    More details can be found in `ticket 217
-    <https://github.com/jborg/attic/issues/217>`_ that led to the fork.
+More details can be found in `ticket 217
+<https://github.com/jborg/attic/issues/217>`_ that led to the fork.
 
 
-    Borg intends to be:
+Borg intends to be:
 
 
-    * simple:
+* simple:
 
 
-      * as simple as possible, but no simpler
-      * do the right thing by default, but offer options
-    * open:
+  * as simple as possible, but no simpler
+  * do the right thing by default, but offer options
+* open:
 
 
-      * welcome feature requests
-      * accept pull requests of good quality and coding style
-      * give feedback on PRs that can't be accepted "as is"
-      * discuss openly, don't work in the dark
-    * changing:
+  * welcome feature requests
+  * accept pull requests of good quality and coding style
+  * give feedback on PRs that can't be accepted "as is"
+  * discuss openly, don't work in the dark
+* changing:
 
 
-      * Borg is not compatible with Attic
-      * do not break compatibility accidentally, without a good reason
-        or without warning. allow compatibility breaking for other cases.
-      * if major version number changes, it may have incompatible changes
+  * Borg is not compatible with Attic
+  * do not break compatibility accidentally, without a good reason
+    or without warning. allow compatibility breaking for other cases.
+  * if major version number changes, it may have incompatible changes

+ 2 - 2
docs/global.rst.inc

@@ -16,12 +16,12 @@
 .. _libattr: http://savannah.nongnu.org/projects/attr/
 .. _libattr: http://savannah.nongnu.org/projects/attr/
 .. _liblz4: https://github.com/Cyan4973/lz4
 .. _liblz4: https://github.com/Cyan4973/lz4
 .. _OpenSSL: https://www.openssl.org/
 .. _OpenSSL: https://www.openssl.org/
-.. _Python: http://www.python.org/
+.. _`Python 3`: http://www.python.org/
 .. _Buzhash: https://en.wikipedia.org/wiki/Buzhash
 .. _Buzhash: https://en.wikipedia.org/wiki/Buzhash
 .. _msgpack: http://msgpack.org/
 .. _msgpack: http://msgpack.org/
 .. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/
 .. _`msgpack-python`: https://pypi.python.org/pypi/msgpack-python/
 .. _llfuse: https://pypi.python.org/pypi/llfuse/
 .. _llfuse: https://pypi.python.org/pypi/llfuse/
-.. _homebrew: http://mxcl.github.io/homebrew/
+.. _homebrew: http://brew.sh/
 .. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
 .. _userspace filesystems: https://en.wikipedia.org/wiki/Filesystem_in_Userspace
 .. _librelist: http://librelist.com/
 .. _librelist: http://librelist.com/
 .. _Cython: http://cython.org/
 .. _Cython: http://cython.org/

+ 3 - 2
docs/index.rst

@@ -4,10 +4,11 @@
 Borg Documentation
 Borg Documentation
 ==================
 ==================
 
 
+.. include:: ../README.rst
+
 .. toctree::
 .. toctree::
    :maxdepth: 2
    :maxdepth: 2
 
 
-   intro
    installation
    installation
    quickstart
    quickstart
    usage
    usage
@@ -16,4 +17,4 @@ Borg Documentation
    changes
    changes
    internals
    internals
    development
    development
-   api
+   authors

+ 109 - 130
docs/installation.rst

@@ -4,153 +4,126 @@
 Installation
 Installation
 ============
 ============
 
 
-|project_name| pyinstaller binary installation requires:
-
-* Linux: glibc >= 2.12 (ok for most supported Linux releases)
-* MacOS X: 10.10 (unknown whether it works for older releases)
-* FreeBSD: 10.2 (unknown whether it works for older releases)
-
-|project_name| non-binary installation requires:
-
-* Python_ >= 3.2.2
-* OpenSSL_ >= 1.0.0
-* libacl_ (that pulls in libattr_ also)
-* liblz4_
-* some python dependencies, see install_requires in setup.py
-
-General notes
--------------
-You need to do some platform specific preparation steps (to install libraries
-and tools) followed by the generic installation of |project_name| itself:
-
-Below, we describe different ways to install |project_name|.
-
-- **dist package** - easy and fast, needs a distribution and platform specific
-  binary package (for your Linux/*BSD/OS X/... distribution).
-- **pyinstaller binary** - easy and fast, we provide a ready-to-use binary file
-  that just works on the supported platforms
-- **pypi** - installing a source package from pypi needs more installation steps
-  and will need a compiler, development headers, etc..
+There are different ways to install |project_name|:
+
+- **distribution package** - easy and fast if a package is available for your
+  Linux/BSD distribution.
+- **PyInstaller binary** - easy and fast, we provide a ready-to-use binary file
+  that comes bundled with all dependencies.
+- **pip** - installing a source package with pip needs more installation steps
+  and requires all dependencies with development headers and a compiler.
 - **git** - for developers and power users who want to have the latest code or
 - **git** - for developers and power users who want to have the latest code or
   use revision control (each release is tagged).
   use revision control (each release is tagged).
 
 
-**Python 3**: Even though this is not the default Python version on many systems,
-it is usually available as an optional install.
 
 
-Virtualenv_ can be used to build and install |project_name| without affecting
-the system Python or requiring root access.
-
-Important:
-If you install into a virtual environment, you need to **activate**
-the virtual env first (``source borg-env/bin/activate``).
-Alternatively, directly run ``borg-env/bin/borg`` (or symlink that into some
-directory that is in your PATH so you can just run ``borg``).
-Using a virtual environment is optional, but recommended except for the most
-simple use cases.
-
-The llfuse_ python package is also required if you wish to mount an
-archive as a FUSE filesystem. Only FUSE >= 2.8.0 can support llfuse.
+Installation (Distribution Package)
+-----------------------------------
 
 
-You only need **Cython** to compile the .pyx files to the respective .c files
-when using |project_name| code from git. For |project_name| releases, the .c
-files will be bundled, so you won't need Cython to install a release.
+Some Linux and BSD distributions might offer a ready-to-use ``borgbackup``
+package which can be installed with the package manager.  As |project_name| is
+still a young project, such a package might be not available for your system
+yet. Please ask package maintainers to build a package or, if you can package /
+submit it yourself, please help us with that!
 
 
-Platform notes
---------------
-FreeBSD: You may need to get a recent enough OpenSSL version from FreeBSD ports.
+* On **Arch Linux**, there is a package available in the AUR_.
 
 
-Mac OS X: You may need to get a recent enough OpenSSL version from homebrew_.
+If a package is available, it might be interesting to check its version
+and compare that to our latest release and review the :doc:`changes`.
 
 
-Mac OS X: You need OS X FUSE >= 3.0.
+.. _AUR: https://aur.archlinux.org/packages/borgbackup/
 
 
 
 
-Installation (dist package)
----------------------------
-Some Linux, BSD and OS X distributions might offer a ready-to-use
-`borgbackup` package (which can be easily installed in the usual way).
-
-As |project_name| is still relatively new, such a package might be not
-available for your system yet. Please ask package maintainers to build a
-package or, if you can package / submit it yourself, please help us with
-that!
-
-If a package is available, it might be interesting for you to check its version
-and compare that to our latest release and review the change log (see links on
-our web site).
-
-
-Installation (pyinstaller binary)
+Installation (PyInstaller Binary)
 ---------------------------------
 ---------------------------------
-For some platforms we offer a ready-to-use standalone borg binary.
 
 
-It is supposed to work without requiring installation or preparations.
+The |project_name| binary is available on the releases_ page for the following
+platforms:
 
 
-Check https://github.com/borgbackup/borg/issues/214 for available binaries.
+* **Linux**: glibc >= 2.13 (ok for most supported Linux releases)
+* **Mac OS X**: 10.10 (unknown whether it works for older releases)
+* **FreeBSD**: 10.2 (unknown whether it works for older releases)
 
 
+These binaries work without requiring specific installation steps. Just drop
+them into a directory in your ``PATH`` and then you can run ``borg``. If a new
+version is released, you will have to manually download it and replace the old
+version.
 
 
-Debian Jessie / Ubuntu 14.04 preparations (git/pypi)
-----------------------------------------------------
+.. _releases: https://github.com/borgbackup/borg/releases
 
 
-.. parsed-literal::
+Installing the Dependencies
+---------------------------
 
 
-    # Python 3.x (>= 3.2) + Headers, Py Package Installer, VirtualEnv
-    apt-get install python3 python3-dev python3-pip python-virtualenv
+To install |project_name| from a source package, you have to install the
+following dependencies first:
 
 
-    # we need OpenSSL + Headers for Crypto
-    apt-get install libssl-dev openssl
+* `Python 3`_ >= 3.2.2. Even though Python 3 is not the default Python version on
+  most systems, it is usually available as an optional install.
+* OpenSSL_ >= 1.0.0
+* libacl_ (that pulls in libattr_ also)
+* liblz4_
+* some Python dependencies, pip will automatically install them for you
+* optionally, the llfuse_ Python package is required if you wish to mount an
+  archive as a FUSE filesystem. FUSE >= 2.8.0 is required for llfuse.
 
 
-    # ACL support Headers + Library
-    apt-get install libacl1-dev libacl1
+In the following, the steps needed to install the dependencies are listed for a
+selection of platforms. If your distribution is not covered by these
+instructions, try to use your package manager to install the dependencies.  On
+FreeBSD, you may need to get a recent enough OpenSSL version from FreeBSD
+ports.
 
 
-    # lz4 super fast compression support Headers + Library
-    apt-get install liblz4-dev liblz4-1
+After you have installed the dependencies, you can proceed with steps outlined
+under :ref:`pip-installation`.
 
 
-    # if you do not have gcc / make / etc. yet
-    apt-get install build-essential
+Debian / Ubuntu
+~~~~~~~~~~~~~~~
 
 
-    # optional: FUSE support - to mount backup archives
-    # in case you get complaints about permission denied on /etc/fuse.conf:
-    # on ubuntu this means your user is not in the "fuse" group. just add
-    # yourself there, log out and log in again.
-    apt-get install libfuse-dev fuse pkg-config
+Install the dependencies with development headers::
 
 
-    # optional: for unit testing
-    apt-get install fakeroot
+    sudo apt-get install python3 python3-dev python3-pip python-virtualenv
+    sudo apt-get install libssl-dev openssl
+    sudo apt-get install libacl1-dev libacl1
+    sudo apt-get install liblz4-dev liblz4-1
+    sudo apt-get install build-essential
+    sudo apt-get install libfuse-dev fuse pkg-config    # optional, for FUSE support
 
 
+In case you get complaints about permission denied on ``/etc/fuse.conf``: on
+Ubuntu this means your user is not in the ``fuse`` group. Add yourself to that
+group, log out and log in again.
 
 
-Korora / Fedora 21 preparations (git/pypi)
-------------------------------------------
+Fedora / Korora
+~~~~~~~~~~~~~~~
 
 
-.. parsed-literal::
+Install the dependencies with development headers::
 
 
-    # Python 3.x (>= 3.2) + Headers, Py Package Installer, VirtualEnv
     sudo dnf install python3 python3-devel python3-pip python3-virtualenv
     sudo dnf install python3 python3-devel python3-pip python3-virtualenv
-
-    # we need OpenSSL + Headers for Crypto
     sudo dnf install openssl-devel openssl
     sudo dnf install openssl-devel openssl
-
-    # ACL support Headers + Library
     sudo dnf install libacl-devel libacl
     sudo dnf install libacl-devel libacl
-
-    # lz4 super fast compression support Headers + Library
     sudo dnf install lz4-devel
     sudo dnf install lz4-devel
+    sudo dnf install fuse-devel fuse pkgconfig         # optional, for FUSE support
+
+
+Mac OS X
+~~~~~~~~
+
+Assuming you have installed homebrew_, the following steps will install all the
+dependencies::
 
 
-    # optional: FUSE support - to mount backup archives
-    sudo dnf install fuse-devel fuse pkgconfig
-    
-    # optional: for unit testing
-    sudo dnf install fakeroot
+    brew install python3 lz4 openssl
+    pip3 install virtualenv
 
 
+For FUSE support to mount the backup archives, you need at least version 3.0 of
+FUSE for OS X, which is available as a pre-release_.
 
 
-Cygwin preparations (git/pypi)
-------------------------------
+.. _pre-release: https://github.com/osxfuse/osxfuse/releases
 
 
-Please note that running under cygwin is rather experimental, stuff has been
-tested with CygWin (x86-64) v2.1.0.
+Cygwin
+~~~~~~
 
 
-You'll need at least (use the cygwin installer to fetch/install these):
+.. note::
+    Running under Cygwin is experimental and has only been tested with Cygwin
+    (x86-64) v2.1.0.
 
 
-::
+Use the Cygwin installer to install the dependencies::
 
 
     python3 python3-setuptools
     python3 python3-setuptools
     python3-cython  # not needed for releases
     python3-cython  # not needed for releases
@@ -159,50 +132,55 @@ You'll need at least (use the cygwin installer to fetch/install these):
     liblz4_1 liblz4-devel  # from cygwinports.org
     liblz4_1 liblz4-devel  # from cygwinports.org
     git make openssh
     git make openssh
 
 
-You can then install ``pip`` and ``virtualenv``:
-
-::
+You can then install ``pip`` and ``virtualenv``::
 
 
     easy_install-3.4 pip
     easy_install-3.4 pip
     pip install virtualenv
     pip install virtualenv
 
 
-And now continue with the generic installation (see below).
+In case the creation of the virtual environment fails, try deleting this file::
 
 
-In case that creation of the virtual env fails, try deleting this file:
+    /usr/lib/python3.4/__pycache__/platform.cpython-34.pyc
 
 
-::
 
 
-    /usr/lib/python3.4/__pycache__/platform.cpython-34.pyc
+.. _pip-installation:
 
 
+Installation (pip)
+------------------
 
 
-Installation (pypi)
--------------------
+Virtualenv_ can be used to build and install |project_name| without affecting
+the system Python or requiring root access.  Using a virtual environment is
+optional, but recommended except for the most simple use cases.
 
 
-This uses the latest (source package) release from PyPi.
+.. note::
+    If you install into a virtual environment, you need to **activate** it
+    first (``source borg-env/bin/activate``), before running ``borg``.
+    Alternatively, symlink ``borg-env/bin/borg`` into some directory that is in
+    your ``PATH`` so you can just run ``borg``.
 
 
-.. parsed-literal::
+This will use ``pip`` to install the latest release from PyPi::
 
 
     virtualenv --python=python3 borg-env
     virtualenv --python=python3 borg-env
-    source borg-env/bin/activate   # always before using!
+    source borg-env/bin/activate
 
 
-    # install borg + dependencies into virtualenv
+    # install Borg + Python dependencies into virtualenv
     pip install 'llfuse<0.41'  # optional, for FUSE support
     pip install 'llfuse<0.41'  # optional, for FUSE support
                                # 0.41 and 0.41.1 have unicode issues at install time
                                # 0.41 and 0.41.1 have unicode issues at install time
     pip install borgbackup
     pip install borgbackup
 
 
-Note: we install into a virtual environment here, but this is not a requirement.
+To upgrade |project_name| to a new version later, run the following after
+activating your virtual environment::
+
+    pip install -U borgbackup
 
 
 
 
 Installation (git)
 Installation (git)
 ------------------
 ------------------
 
 
 This uses latest, unreleased development code from git.
 This uses latest, unreleased development code from git.
-While we try not to break master, there are no guarantees on anything.
-
-.. parsed-literal::
+While we try not to break master, there are no guarantees on anything. ::
 
 
-    # get |project_name| from github, install it
-    git clone |git_url|
+    # get borg from github
+    git clone https://github.com/borgbackup/borg.git
 
 
     virtualenv --python=python3 borg-env
     virtualenv --python=python3 borg-env
     source borg-env/bin/activate   # always before using!
     source borg-env/bin/activate   # always before using!
@@ -216,6 +194,7 @@ While we try not to break master, there are no guarantees on anything.
     pip install -e .  # in-place editable mode
     pip install -e .  # in-place editable mode
 
 
     # optional: run all the tests, on all supported Python versions
     # optional: run all the tests, on all supported Python versions
+    # requires fakeroot, available through your package manager
     fakeroot -u tox
     fakeroot -u tox
 
 
-Note: as a developer or power user, you always want to use a virtual environment.
+.. note:: As a developer or power user, you always want to use a virtual environment.

+ 0 - 7
docs/intro.rst

@@ -1,7 +0,0 @@
-.. include:: global.rst.inc
-.. _foreword:
-
-Introduction
-============
-
-.. include:: ../README.rst

+ 5550 - 0
docs/misc/asciinema/install_and_basics.json

@@ -0,0 +1,5550 @@
+{
+  "version": 1,
+  "width": 80,
+  "height": 24,
+  "duration": 332.0,
+  "command": "/bin/bash",
+  "title": "borgbackup - installation and basic usage",
+  "env": {
+    "TERM": "xterm",
+    "SHELL": "/bin/bash"
+  },
+  "stdout": [
+    [
+      0.083341,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.349103,
+      "#"
+    ],
+    [
+      0.338948,
+      " "
+    ],
+    [
+      0.185424,
+      "b"
+    ],
+    [
+      0.142971,
+      "o"
+    ],
+    [
+      0.091227,
+      "r"
+    ],
+    [
+      0.092867,
+      "g"
+    ],
+    [
+      0.222552,
+      "b"
+    ],
+    [
+      0.114706,
+      "a"
+    ],
+    [
+      0.125044,
+      "c"
+    ],
+    [
+      0.144755,
+      "k"
+    ],
+    [
+      0.241044,
+      "u"
+    ],
+    [
+      0.243681,
+      "p"
+    ],
+    [
+      0.265888,
+      " "
+    ],
+    [
+      0.345247,
+      "-"
+    ],
+    [
+      0.251918,
+      " "
+    ],
+    [
+      0.233420,
+      "i"
+    ],
+    [
+      0.078609,
+      "n"
+    ],
+    [
+      0.076809,
+      "s"
+    ],
+    [
+      0.070225,
+      "t"
+    ],
+    [
+      0.148413,
+      "a"
+    ],
+    [
+      0.077403,
+      "l"
+    ],
+    [
+      0.139884,
+      "l"
+    ],
+    [
+      0.084807,
+      "a"
+    ],
+    [
+      0.138823,
+      "t"
+    ],
+    [
+      0.068185,
+      "i"
+    ],
+    [
+      0.170422,
+      "o"
+    ],
+    [
+      0.161091,
+      "n"
+    ],
+    [
+      0.169247,
+      " "
+    ],
+    [
+      0.110722,
+      "a"
+    ],
+    [
+      0.113785,
+      "n"
+    ],
+    [
+      0.397895,
+      "d"
+    ],
+    [
+      0.305048,
+      " "
+    ],
+    [
+      0.211476,
+      "b"
+    ],
+    [
+      0.109865,
+      "a"
+    ],
+    [
+      0.230634,
+      "s"
+    ],
+    [
+      0.277915,
+      "i"
+    ],
+    [
+      0.206167,
+      "c"
+    ],
+    [
+      0.145265,
+      " "
+    ],
+    [
+      0.219619,
+      "u"
+    ],
+    [
+      0.139989,
+      "s"
+    ],
+    [
+      0.180240,
+      "a"
+    ],
+    [
+      0.200391,
+      "g"
+    ],
+    [
+      0.116961,
+      "e"
+    ],
+    [
+      0.172074,
+      "\r\n"
+    ],
+    [
+      0.000449,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.620909,
+      "#"
+    ],
+    [
+      0.217833,
+      " "
+    ],
+    [
+      0.592920,
+      "I"
+    ],
+    [
+      0.166726,
+      " "
+    ],
+    [
+      0.161953,
+      "h"
+    ],
+    [
+      0.072501,
+      "a"
+    ],
+    [
+      0.170951,
+      "v"
+    ],
+    [
+      0.154067,
+      "e"
+    ],
+    [
+      0.110535,
+      " "
+    ],
+    [
+      0.155235,
+      "a"
+    ],
+    [
+      0.130825,
+      "l"
+    ],
+    [
+      0.111834,
+      "r"
+    ],
+    [
+      0.142378,
+      "e"
+    ],
+    [
+      0.165867,
+      "a"
+    ],
+    [
+      0.062556,
+      "d"
+    ],
+    [
+      0.091778,
+      "y"
+    ],
+    [
+      0.216280,
+      " "
+    ],
+    [
+      0.169501,
+      "d"
+    ],
+    [
+      0.198240,
+      "o"
+    ],
+    [
+      0.092373,
+      "w"
+    ],
+    [
+      0.143405,
+      "n"
+    ],
+    [
+      0.207324,
+      "l"
+    ],
+    [
+      0.164248,
+      "o"
+    ],
+    [
+      0.088481,
+      "a"
+    ],
+    [
+      0.129191,
+      "d"
+    ],
+    [
+      0.179234,
+      "e"
+    ],
+    [
+      0.189248,
+      "d"
+    ],
+    [
+      0.145203,
+      " "
+    ],
+    [
+      0.221625,
+      "t"
+    ],
+    [
+      0.100064,
+      "h"
+    ],
+    [
+      0.133349,
+      "e"
+    ],
+    [
+      0.066501,
+      " "
+    ],
+    [
+      0.187004,
+      "f"
+    ],
+    [
+      0.142461,
+      "i"
+    ],
+    [
+      0.204723,
+      "l"
+    ],
+    [
+      0.068716,
+      "e"
+    ],
+    [
+      0.237576,
+      "s"
+    ],
+    [
+      0.128085,
+      ":"
+    ],
+    [
+      0.242282,
+      "\r\n"
+    ],
+    [
+      0.000327,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      1.796834,
+      "l"
+    ],
+    [
+      0.092545,
+      "s"
+    ],
+    [
+      0.210322,
+      " "
+    ],
+    [
+      0.189710,
+      "-"
+    ],
+    [
+      0.215532,
+      "l"
+    ],
+    [
+      0.852863,
+      "\r\n"
+    ],
+    [
+      0.002104,
+      "total 10620\r\n"
+    ],
+    [
+      0.000040,
+      "-rw-rw-r-- 1 tw tw 10869049 Oct 24 22:11 borg-linux64"
+    ],
+    [
+      0.000007,
+      "\r\n"
+    ],
+    [
+      0.000019,
+      "-rw-rw-r-- 1 tw tw      819 Oct 24 22:11 borg-linux64.asc\r\n"
+    ],
+    [
+      0.000431,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.513172,
+      "#"
+    ],
+    [
+      0.284059,
+      " "
+    ],
+    [
+      0.330931,
+      "b"
+    ],
+    [
+      0.118806,
+      "i"
+    ],
+    [
+      0.100553,
+      "n"
+    ],
+    [
+      0.259930,
+      "a"
+    ],
+    [
+      0.106715,
+      "r"
+    ],
+    [
+      0.276545,
+      "y"
+    ],
+    [
+      0.126132,
+      " "
+    ],
+    [
+      0.379724,
+      "+"
+    ],
+    [
+      0.199249,
+      " "
+    ],
+    [
+      0.295913,
+      "G"
+    ],
+    [
+      0.108970,
+      "P"
+    ],
+    [
+      0.080480,
+      "G"
+    ],
+    [
+      0.349293,
+      " "
+    ],
+    [
+      0.236785,
+      "s"
+    ],
+    [
+      0.105197,
+      "i"
+    ],
+    [
+      0.289951,
+      "g"
+    ],
+    [
+      0.351385,
+      "n"
+    ],
+    [
+      0.282003,
+      "a"
+    ],
+    [
+      0.206591,
+      "t"
+    ],
+    [
+      0.163963,
+      "u"
+    ],
+    [
+      0.082416,
+      "r"
+    ],
+    [
+      0.125432,
+      "e"
+    ],
+    [
+      0.369988,
+      "\r\n"
+    ],
+    [
+      0.000341,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.889617,
+      "#"
+    ],
+    [
+      0.226974,
+      " "
+    ],
+    [
+      0.218497,
+      "l"
+    ],
+    [
+      0.134545,
+      "e"
+    ],
+    [
+      0.103159,
+      "t"
+    ],
+    [
+      0.711682,
+      "'"
+    ],
+    [
+      0.185463,
+      "s"
+    ],
+    [
+      0.162130,
+      " "
+    ],
+    [
+      0.166049,
+      "v"
+    ],
+    [
+      0.183069,
+      "e"
+    ],
+    [
+      0.099764,
+      "r"
+    ],
+    [
+      0.234211,
+      "i"
+    ],
+    [
+      0.854328,
+      "f"
+    ],
+    [
+      0.203758,
+      "y"
+    ],
+    [
+      0.166681,
+      " "
+    ],
+    [
+      0.216715,
+      "t"
+    ],
+    [
+      0.560064,
+      "h"
+    ],
+    [
+      0.151837,
+      "a"
+    ],
+    [
+      0.194509,
+      "t"
+    ],
+    [
+      0.119665,
+      " "
+    ],
+    [
+      0.141089,
+      "t"
+    ],
+    [
+      0.096803,
+      "h"
+    ],
+    [
+      0.104718,
+      "e"
+    ],
+    [
+      0.106761,
+      " "
+    ],
+    [
+      0.229401,
+      "b"
+    ],
+    [
+      0.213802,
+      "i"
+    ],
+    [
+      0.075481,
+      "n"
+    ],
+    [
+      0.138720,
+      "a"
+    ],
+    [
+      0.062411,
+      "r"
+    ],
+    [
+      0.292719,
+      "y"
+    ],
+    [
+      0.482737,
+      " "
+    ],
+    [
+      0.211595,
+      "i"
+    ],
+    [
+      0.110964,
+      "s"
+    ],
+    [
+      0.102100,
+      " "
+    ],
+    [
+      0.143380,
+      "v"
+    ],
+    [
+      0.189214,
+      "a"
+    ],
+    [
+      0.099337,
+      "l"
+    ],
+    [
+      0.172757,
+      "i"
+    ],
+    [
+      0.082456,
+      "d"
+    ],
+    [
+      0.177514,
+      ":"
+    ],
+    [
+      0.622492,
+      "\r\n"
+    ],
+    [
+      0.000313,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      2.000000,
+      "g"
+    ],
+    [
+      0.261924,
+      "p"
+    ],
+    [
+      0.108570,
+      "g"
+    ],
+    [
+      0.247315,
+      " "
+    ],
+    [
+      0.277162,
+      "-"
+    ],
+    [
+      0.141397,
+      "-"
+    ],
+    [
+      0.143255,
+      "v"
+    ],
+    [
+      0.162858,
+      "e"
+    ],
+    [
+      0.040051,
+      "r"
+    ],
+    [
+      0.105941,
+      "i"
+    ],
+    [
+      0.144872,
+      "f"
+    ],
+    [
+      0.306497,
+      "y"
+    ],
+    [
+      0.468271,
+      " "
+    ],
+    [
+      2.000000,
+      "b"
+    ],
+    [
+      0.119390,
+      "o"
+    ],
+    [
+      0.463137,
+      "\u0007"
+    ],
+    [
+      0.000095,
+      "rg-linux64"
+    ],
+    [
+      0.341519,
+      "."
+    ],
+    [
+      0.146977,
+      "asc "
+    ],
+    [
+      0.186292,
+      "\r\n"
+    ],
+    [
+      0.100648,
+      "gpg: Signature made Wed 07 Oct 2015 02:41:38 PM CEST\r\n"
+    ],
+    [
+      0.000011,
+      "gpg:                using RSA key 243ACFA951F78E01\r\n"
+    ],
+    [
+      0.006906,
+      "gpg: Good signature from \"Thomas Waldmann \u003ctw@waldmann-edv.de\u003e\""
+    ],
+    [
+      0.000033,
+      "\r\ngpg:                 aka \"Thomas Waldmann \u003ctw-public@gmx.de\u003e\"\r\ngpg:                 aka \"Thomas Waldmann \u003ctwaldmann@thinkmo.de\u003e\"\r\n"
+    ],
+    [
+      0.000018,
+      "gpg:                 aka \"Thomas Waldmann \u003cthomas.j.waldmann@gmail.com\u003e\"\r\n"
+    ],
+    [
+      0.003077,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      2.000000,
+      "#"
+    ],
+    [
+      0.241501,
+      " "
+    ],
+    [
+      0.186571,
+      "e"
+    ],
+    [
+      0.214388,
+      "v"
+    ],
+    [
+      0.157101,
+      "e"
+    ],
+    [
+      0.042348,
+      "r"
+    ],
+    [
+      0.253261,
+      "y"
+    ],
+    [
+      0.254356,
+      "t"
+    ],
+    [
+      0.094622,
+      "h"
+    ],
+    [
+      0.213972,
+      "i"
+    ],
+    [
+      0.084853,
+      "n"
+    ],
+    [
+      0.084920,
+      "g"
+    ],
+    [
+      0.178519,
+      " "
+    ],
+    [
+      0.256151,
+      "o"
+    ],
+    [
+      0.217918,
+      "k"
+    ],
+    [
+      0.153899,
+      "!"
+    ],
+    [
+      0.246211,
+      "\r\n"
+    ],
+    [
+      0.000330,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      2.000000,
+      "#"
+    ],
+    [
+      0.288008,
+      " "
+    ],
+    [
+      0.232836,
+      "i"
+    ],
+    [
+      0.055326,
+      "n"
+    ],
+    [
+      0.142978,
+      "s"
+    ],
+    [
+      0.080599,
+      "t"
+    ],
+    [
+      0.139018,
+      "a"
+    ],
+    [
+      0.111052,
+      "l"
+    ],
+    [
+      0.132419,
+      "l"
+    ],
+    [
+      0.169037,
+      " "
+    ],
+    [
+      0.117036,
+      "t"
+    ],
+    [
+      0.092749,
+      "h"
+    ],
+    [
+      0.124768,
+      "e"
+    ],
+    [
+      0.088888,
+      " "
+    ],
+    [
+      0.184118,
+      "b"
+    ],
+    [
+      0.182336,
+      "i"
+    ],
+    [
+      0.075466,
+      "n"
+    ],
+    [
+      0.085516,
+      "a"
+    ],
+    [
+      0.060363,
+      "r"
+    ],
+    [
+      0.843225,
+      "y"
+    ],
+    [
+      0.209758,
+      " "
+    ],
+    [
+      0.168892,
+      "a"
+    ],
+    [
+      0.151126,
+      "s"
+    ],
+    [
+      0.127487,
+      " "
+    ],
+    [
+      0.300923,
+      "\""
+    ],
+    [
+      0.217060,
+      "b"
+    ],
+    [
+      0.221579,
+      "o"
+    ],
+    [
+      0.047775,
+      "r"
+    ],
+    [
+      0.107202,
+      "g"
+    ],
+    [
+      0.438939,
+      "\""
+    ],
+    [
+      0.253153,
+      ":"
+    ],
+    [
+      0.617823,
+      "\r\n"
+    ],
+    [
+      0.000326,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.816740,
+      "c"
+    ],
+    [
+      0.168734,
+      "p"
+    ],
+    [
+      0.230846,
+      " "
+    ],
+    [
+      0.299588,
+      "b"
+    ],
+    [
+      0.121082,
+      "o"
+    ],
+    [
+      0.214148,
+      "\u0007"
+    ],
+    [
+      0.000011,
+      "rg-linux64"
+    ],
+    [
+      0.331736,
+      " "
+    ],
+    [
+      0.812264,
+      "~"
+    ],
+    [
+      0.518926,
+      "/"
+    ],
+    [
+      0.233797,
+      "b"
+    ],
+    [
+      0.214141,
+      "i"
+    ],
+    [
+      0.098062,
+      "n"
+    ],
+    [
+      0.607725,
+      "/"
+    ],
+    [
+      0.566434,
+      "b"
+    ],
+    [
+      0.145886,
+      "o"
+    ],
+    [
+      0.113081,
+      "r"
+    ],
+    [
+      0.068870,
+      "g"
+    ],
+    [
+      0.851794,
+      "\r\n"
+    ],
+    [
+      0.012632,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      2.000000,
+      "#"
+    ],
+    [
+      0.269926,
+      " "
+    ],
+    [
+      0.208575,
+      "m"
+    ],
+    [
+      0.135192,
+      "a"
+    ],
+    [
+      0.119543,
+      "k"
+    ],
+    [
+      0.080873,
+      "e"
+    ],
+    [
+      0.156871,
+      " "
+    ],
+    [
+      0.197124,
+      "i"
+    ],
+    [
+      0.078784,
+      "t"
+    ],
+    [
+      0.142373,
+      " "
+    ],
+    [
+      0.189080,
+      "e"
+    ],
+    [
+      0.232597,
+      "x"
+    ],
+    [
+      0.170105,
+      "e"
+    ],
+    [
+      0.132039,
+      "c"
+    ],
+    [
+      0.230568,
+      "u"
+    ],
+    [
+      0.086573,
+      "t"
+    ],
+    [
+      0.255047,
+      "a"
+    ],
+    [
+      0.231478,
+      "b"
+    ],
+    [
+      0.283723,
+      "l"
+    ],
+    [
+      0.112987,
+      "e"
+    ],
+    [
+      0.518611,
+      ":"
+    ],
+    [
+      0.459423,
+      "\r\n"
+    ],
+    [
+      0.000822,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.353739,
+      "c"
+    ],
+    [
+      0.114161,
+      "h"
+    ],
+    [
+      0.268562,
+      "m"
+    ],
+    [
+      0.179085,
+      "o"
+    ],
+    [
+      0.145360,
+      "d"
+    ],
+    [
+      0.075599,
+      " "
+    ],
+    [
+      0.773964,
+      "+"
+    ],
+    [
+      0.113699,
+      "x"
+    ],
+    [
+      0.187579,
+      " "
+    ],
+    [
+      0.381391,
+      "~"
+    ],
+    [
+      0.512520,
+      "/"
+    ],
+    [
+      0.231090,
+      "b"
+    ],
+    [
+      0.197636,
+      "i"
+    ],
+    [
+      0.101238,
+      "n"
+    ],
+    [
+      0.341295,
+      "/"
+    ],
+    [
+      0.306047,
+      "b"
+    ],
+    [
+      0.106898,
+      "o"
+    ],
+    [
+      0.233773,
+      "rg "
+    ],
+    [
+      0.519336,
+      "\r\n"
+    ],
+    [
+      0.001408,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      2.000000,
+      "#"
+    ],
+    [
+      0.247104,
+      " "
+    ],
+    [
+      0.218717,
+      "i"
+    ],
+    [
+      0.067769,
+      "n"
+    ],
+    [
+      0.139583,
+      "s"
+    ],
+    [
+      0.092034,
+      "t"
+    ],
+    [
+      0.152729,
+      "a"
+    ],
+    [
+      0.083844,
+      "l"
+    ],
+    [
+      0.145806,
+      "l"
+    ],
+    [
+      0.120879,
+      "a"
+    ],
+    [
+      0.164967,
+      "t"
+    ],
+    [
+      0.065308,
+      "i"
+    ],
+    [
+      0.816983,
+      "o"
+    ],
+    [
+      0.231669,
+      "n"
+    ],
+    [
+      0.185168,
+      " "
+    ],
+    [
+      0.125214,
+      "d"
+    ],
+    [
+      0.112630,
+      "o"
+    ],
+    [
+      0.068650,
+      "n"
+    ],
+    [
+      0.108386,
+      "e"
+    ],
+    [
+      0.563031,
+      "!"
+    ],
+    [
+      2.000000,
+      "\r\n"
+    ],
+    [
+      0.000365,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.347093,
+      "#"
+    ],
+    [
+      0.262764,
+      " "
+    ],
+    [
+      0.191568,
+      "l"
+    ],
+    [
+      0.086614,
+      "e"
+    ],
+    [
+      0.110365,
+      "t"
+    ],
+    [
+      0.707057,
+      "'"
+    ],
+    [
+      0.220060,
+      "s"
+    ],
+    [
+      0.181690,
+      " "
+    ],
+    [
+      0.128039,
+      "c"
+    ],
+    [
+      0.176264,
+      "r"
+    ],
+    [
+      0.171208,
+      "e"
+    ],
+    [
+      0.199371,
+      "a"
+    ],
+    [
+      0.161622,
+      "t"
+    ],
+    [
+      0.145989,
+      "e"
+    ],
+    [
+      0.187920,
+      " "
+    ],
+    [
+      0.734653,
+      "a"
+    ],
+    [
+      0.185812,
+      " "
+    ],
+    [
+      0.270851,
+      "r"
+    ],
+    [
+      0.120000,
+      "e"
+    ],
+    [
+      0.161097,
+      "p"
+    ],
+    [
+      0.179813,
+      "o"
+    ],
+    [
+      0.170557,
+      "s"
+    ],
+    [
+      0.145457,
+      "i"
+    ],
+    [
+      0.165200,
+      "t"
+    ],
+    [
+      0.135578,
+      "o"
+    ],
+    [
+      0.130363,
+      "r"
+    ],
+    [
+      0.461631,
+      "y"
+    ],
+    [
+      0.303047,
+      ":"
+    ],
+    [
+      0.955198,
+      "\r\n"
+    ],
+    [
+      0.000300,
+      "\u001b]0;tw@tux: ~/borg/demo/download\u0007tw@tux:~/borg/demo/download$ "
+    ],
+    [
+      0.301237,
+      "c"
+    ],
+    [
+      0.084267,
+      "d"
+    ],
+    [
+      0.155241,
+      " "
+    ],
+    [
+      0.813751,
+      "."
+    ],
+    [
+      0.157147,
+      "."
+    ],
+    [
+      0.573720,
+      "\r\n"
+    ],
+    [
+      0.000508,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.225463,
+      "b"
+    ],
+    [
+      0.274841,
+      "o"
+    ],
+    [
+      0.125292,
+      "r"
+    ],
+    [
+      0.083313,
+      "g"
+    ],
+    [
+      0.088596,
+      " "
+    ],
+    [
+      0.231502,
+      "i"
+    ],
+    [
+      0.062726,
+      "n"
+    ],
+    [
+      0.144877,
+      "i"
+    ],
+    [
+      0.112508,
+      "t"
+    ],
+    [
+      0.313489,
+      " "
+    ],
+    [
+      0.298944,
+      "r"
+    ],
+    [
+      0.216556,
+      "e"
+    ],
+    [
+      0.180484,
+      "p"
+    ],
+    [
+      0.204141,
+      "o"
+    ],
+    [
+      0.682782,
+      "\r\n"
+    ],
+    [
+      0.352828,
+      "Initializing repository at \"repo\"\r\n"
+    ],
+    [
+      0.001407,
+      "Encryption NOT enabled.\r\nUse the \"--encryption=repokey|keyfile|passphrase\" to enable encryption."
+    ],
+    [
+      0.000009,
+      "\r\n"
+    ],
+    [
+      0.008492,
+      "Synchronizing chunks cache..."
+    ],
+    [
+      0.000009,
+      "\r\n"
+    ],
+    [
+      0.000030,
+      "Archives: 0, w/ cached Idx: 0, w/ outdated Idx: 0, w/o cached Idx: 0."
+    ],
+    [
+      0.000004,
+      "\r\n"
+    ],
+    [
+      0.000024,
+      "Done."
+    ],
+    [
+      0.000004,
+      "\r\n"
+    ],
+    [
+      0.027827,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.988184,
+      "#"
+    ],
+    [
+      0.248844,
+      " "
+    ],
+    [
+      0.199486,
+      "l"
+    ],
+    [
+      0.104455,
+      "e"
+    ],
+    [
+      0.127960,
+      "t"
+    ],
+    [
+      0.484976,
+      "'"
+    ],
+    [
+      0.186103,
+      "s"
+    ],
+    [
+      0.151763,
+      " "
+    ],
+    [
+      0.177456,
+      "c"
+    ],
+    [
+      0.178972,
+      "r"
+    ],
+    [
+      0.183533,
+      "e"
+    ],
+    [
+      0.192725,
+      "a"
+    ],
+    [
+      0.146352,
+      "t"
+    ],
+    [
+      0.156199,
+      "e"
+    ],
+    [
+      0.232699,
+      " "
+    ],
+    [
+      0.513490,
+      "o"
+    ],
+    [
+      0.229828,
+      "u"
+    ],
+    [
+      0.104744,
+      "r"
+    ],
+    [
+      0.115068,
+      " "
+    ],
+    [
+      0.201439,
+      "f"
+    ],
+    [
+      0.333315,
+      "i"
+    ],
+    [
+      0.209070,
+      "r"
+    ],
+    [
+      0.259194,
+      "s"
+    ],
+    [
+      0.076346,
+      "t"
+    ],
+    [
+      0.125673,
+      " "
+    ],
+    [
+      0.198575,
+      "b"
+    ],
+    [
+      0.089009,
+      "a"
+    ],
+    [
+      0.238307,
+      "c"
+    ],
+    [
+      0.105568,
+      "k"
+    ],
+    [
+      0.254971,
+      "u"
+    ],
+    [
+      0.318094,
+      "p"
+    ],
+    [
+      0.690770,
+      ":"
+    ],
+    [
+      0.580155,
+      "\r\n"
+    ],
+    [
+      0.000308,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.603046,
+      "b"
+    ],
+    [
+      0.104492,
+      "o"
+    ],
+    [
+      0.148182,
+      "r"
+    ],
+    [
+      0.087024,
+      "g"
+    ],
+    [
+      0.176897,
+      " "
+    ],
+    [
+      0.183168,
+      "c"
+    ],
+    [
+      0.185325,
+      "r"
+    ],
+    [
+      0.183347,
+      "e"
+    ],
+    [
+      0.182868,
+      "a"
+    ],
+    [
+      0.170600,
+      "t"
+    ],
+    [
+      0.137005,
+      "e"
+    ],
+    [
+      0.164357,
+      " "
+    ],
+    [
+      0.427028,
+      "-"
+    ],
+    [
+      0.147791,
+      "-"
+    ],
+    [
+      0.440101,
+      "s"
+    ],
+    [
+      0.177193,
+      "t"
+    ],
+    [
+      0.203817,
+      "a"
+    ],
+    [
+      0.217150,
+      "t"
+    ],
+    [
+      0.229771,
+      "s"
+    ],
+    [
+      0.191220,
+      " "
+    ],
+    [
+      0.269939,
+      "-"
+    ],
+    [
+      0.145163,
+      "-"
+    ],
+    [
+      0.450053,
+      "p"
+    ],
+    [
+      0.165194,
+      "r"
+    ],
+    [
+      0.044264,
+      "o"
+    ],
+    [
+      0.204568,
+      "g"
+    ],
+    [
+      0.104759,
+      "r"
+    ],
+    [
+      0.213137,
+      "e"
+    ],
+    [
+      0.216596,
+      "s"
+    ],
+    [
+      0.163238,
+      "s"
+    ],
+    [
+      0.241084,
+      " "
+    ],
+    [
+      0.300727,
+      "-"
+    ],
+    [
+      0.149156,
+      "-"
+    ],
+    [
+      0.259608,
+      "c"
+    ],
+    [
+      0.120930,
+      "o"
+    ],
+    [
+      0.098838,
+      "m"
+    ],
+    [
+      0.234615,
+      "p"
+    ],
+    [
+      0.084600,
+      "r"
+    ],
+    [
+      0.166072,
+      "e"
+    ],
+    [
+      0.185576,
+      "s"
+    ],
+    [
+      0.159984,
+      "s"
+    ],
+    [
+      0.122793,
+      "i"
+    ],
+    [
+      0.180423,
+      "o"
+    ],
+    [
+      0.196311,
+      "n"
+    ],
+    [
+      0.181682,
+      " "
+    ],
+    [
+      0.242129,
+      "l"
+    ],
+    [
+      0.842020,
+      "z"
+    ],
+    [
+      0.707941,
+      "4"
+    ],
+    [
+      0.180354,
+      " "
+    ],
+    [
+      0.419080,
+      "r"
+    ],
+    [
+      0.189076,
+      "e"
+    ],
+    [
+      0.172527,
+      "p"
+    ],
+    [
+      0.154922,
+      "o"
+    ],
+    [
+      0.728059,
+      ":"
+    ],
+    [
+      0.147089,
+      ":"
+    ],
+    [
+      0.396117,
+      "b"
+    ],
+    [
+      0.090233,
+      "a"
+    ],
+    [
+      0.199537,
+      "c"
+    ],
+    [
+      0.084686,
+      "k"
+    ],
+    [
+      0.278049,
+      "u \r"
+    ],
+    [
+      0.268438,
+      "p"
+    ],
+    [
+      0.491592,
+      "1"
+    ],
+    [
+      0.508588,
+      " "
+    ],
+    [
+      0.174143,
+      "d"
+    ],
+    [
+      0.175430,
+      "a"
+    ],
+    [
+      0.166841,
+      "t"
+    ],
+    [
+      0.127029,
+      "a"
+    ],
+    [
+      0.380593,
+      "\r\n"
+    ],
+    [
+      0.557518,
+      "  2.68 MB O   1.25 MB C   1.25 MB D data/linux-4.1.8/Doc...ia/v4l/func-read.xml\r"
+    ],
+    [
+      0.200102,
+      "  5.37 MB O   2.46 MB C   2.46 MB D data/linux-4.1.8/Documentation/backlight   \r"
+    ],
+    [
+      0.200342,
+      "  6.99 MB O   3.36 MB C   3.36 MB D data/linux-4.1.8/Doc...rm_big_little_dt.txt\r"
+    ],
+    [
+      0.200137,
+      "  7.83 MB O   3.87 MB C   3.87 MB D data/linux-4.1.8/Doc...s/mtd/atmel-nand.txt\r"
+    ],
+    [
+      0.200271,
+      "  8.77 MB O   4.41 MB C   4.41 MB D data/linux-4.1.8/Doc...ngs/soc/fsl/qman.txt\r"
+    ],
+    [
+      0.200577,
+      "  9.99 MB O   5.12 MB C   5.12 MB D data/linux-4.1.8/Doc...ching/cachefiles.txt\r"
+    ],
+    [
+      0.200033,
+      " 12.11 MB O   6.34 MB C   6.33 MB D data/linux-4.1.8/Doc...infiniband/ipoib.txt\r"
+    ],
+    [
+      0.200272,
+      " 15.27 MB O   8.08 MB C   8.08 MB D data/linux-4.1.8/Doc.../networking/team.txt\r"
+    ],
+    [
+      0.200072,
+      " 18.22 MB O   9.72 MB C   9.71 MB D data/linux-4.1.8/Doc...tation/sysctl/vm.txt\r"
+    ],
+    [
+      0.202107,
+      " 21.05 MB O  11.22 MB C  11.21 MB D data/linux-4.1.8/MAINTAINERS               \r"
+    ],
+    [
+      0.200251,
+      " 23.04 MB O  12.20 MB C  12.20 MB D data/linux-4.1.8/arc...de/uapi/asm/unistd.h\r"
+    ],
+    [
+      0.200450,
+      " 25.45 MB O  13.17 MB C  13.17 MB D data/linux-4.1.8/arc.../boot/dts/imx23.dtsi\r"
+    ],
+    [
+      0.200093,
+      " 27.65 MB O  14.01 MB C  14.00 MB D data/linux-4.1.8/arc...omap3-overo-tobi.dts\r"
+    ],
+    [
+      0.200314,
+      " 30.26 MB O  14.89 MB C  14.89 MB D data/linux-4.1.8/arc...ot/dts/tps65910.dtsi\r"
+    ],
+    [
+      0.200003,
+      " 31.90 MB O  15.63 MB C  15.63 MB D data/linux-4.1.8/arc...include/asm/probes.h\r"
+    ],
+    [
+      0.200493,
+      " 34.66 MB O  16.84 MB C  16.83 MB D data/linux-4.1.8/arc...i/include/mach/psc.h\r"
+    ],
+    [
+      0.200675,
+      " 36.62 MB O  17.70 MB C  17.70 MB D data/linux-4.1.8/arc...mach-ixp4xx/common.c\r"
+    ],
+    [
+      0.200307,
+      " 38.40 MB O  18.54 MB C  18.53 MB D data/linux-4.1.8/arch/arm/mach-omap2/cm.h  \r"
+    ],
+    [
+      0.200254,
+      " 41.29 MB O  19.63 MB C  19.63 MB D data/linux-4.1.8/arch/arm/mach-pxa/idp.c   \r"
+    ],
+    [
+      0.219493,
+      " 43.57 MB O  20.67 MB C  20.66 MB D data/linux-4.1.8/arc...bile/clock-r8a7778.c\r"
+    ],
+    [
+      0.200451,
+      " 45.55 MB O  21.59 MB C  21.59 MB D data/linux-4.1.8/arc...m/plat-samsung/adc.c\r"
+    ],
+    [
+      0.200370,
+      " 47.50 MB O  22.51 MB C  22.51 MB D data/linux-4.1.8/arch/arm64/lib/memmove.S  \r"
+    ],
+    [
+      0.200686,
+      " 49.21 MB O  23.33 MB C  23.32 MB D data/linux-4.1.8/arc...ckfin/kernel/trace.c\r"
+    ],
+    [
+      0.200393,
+      " 53.22 MB O  24.51 MB C  24.50 MB D data/linux-4.1.8/arch/c6x/include/asm/soc.h\r"
+    ],
+    [
+      0.200371,
+      " 56.19 MB O  25.50 MB C  25.49 MB D data/linux-4.1.8/arc...op/iop_sw_cpu_defs.h\r"
+    ],
+    [
+      0.200450,
+      " 57.84 MB O  26.17 MB C  26.14 MB D data/linux-4.1.8/arc...include/asm/vm_mmu.h\r"
+    ],
+    [
+      0.200573,
+      " 60.21 MB O  27.27 MB C  27.25 MB D data/linux-4.1.8/arch/ia64/kernel/time.c   \r"
+    ],
+    [
+      0.200222,
+      " 62.31 MB O  28.18 MB C  28.15 MB D data/linux-4.1.8/arc.../coldfire/sltimers.c\r"
+    ],
+    [
+      0.200756,
+      " 67.09 MB O  29.98 MB C  29.90 MB D data/linux-4.1.8/arc...8k/include/asm/tlb.h\r"
+    ],
+    [
+      0.200716,
+      " 68.75 MB O  30.80 MB C  30.72 MB D data/linux-4.1.8/arc...ude/uapi/asm/fcntl.h\r"
+    ],
+    [
+      0.200734,
+      " 70.69 MB O  31.67 MB C  31.59 MB D data/linux-4.1.8/arc...figs/malta_defconfig\r"
+    ],
+    [
+      0.200198,
+      " 72.12 MB O  32.34 MB C  32.26 MB D data/linux-4.1.8/arc...de/asm/mc146818rtc.h\r"
+    ],
+    [
+      0.200446,
+      " 76.01 MB O  33.45 MB C  33.37 MB D data/linux-4.1.8/arch/mips/jazz/jazzdma.c  \r"
+    ],
+    [
+      0.200111,
+      " 78.19 MB O  34.46 MB C  34.38 MB D data/linux-4.1.8/arc...tlogic/common/time.c\r"
+    ],
+    [
+      0.200191,
+      " 79.84 MB O  35.30 MB C  35.21 MB D data/linux-4.1.8/arc...de/uapi/asm/msgbuf.h\r"
+    ],
+    [
+      0.200421,
+      " 81.35 MB O  36.07 MB C  35.99 MB D data/linux-4.1.8/arc...sc/include/asm/rtc.h\r"
+    ],
+    [
+      0.200090,
+      " 83.49 MB O  37.03 MB C  36.95 MB D data/linux-4.1.8/arc...fsl/qoriq-dma-1.dtsi\r"
+    ],
+    [
+      0.200331,
+      " 85.13 MB O  37.80 MB C  37.72 MB D data/linux-4.1.8/arc...pc836x_rdk_defconfig\r"
+    ],
+    [
+      0.200114,
+      " 87.04 MB O  38.71 MB C  38.63 MB D data/linux-4.1.8/arc...ude/uapi/asm/nvram.h\r"
+    ],
+    [
+      0.200280,
+      " 90.24 MB O  40.19 MB C  40.11 MB D data/linux-4.1.8/arc...pc/math-emu/mtfsfi.c\r"
+    ],
+    [
+      0.216796,
+      " 92.68 MB O  41.41 MB C  41.33 MB D data/linux-4.1.8/arc...rms/powermac/nvram.c\r"
+    ],
+    [
+      0.200198,
+      " 95.32 MB O  42.60 MB C  42.52 MB D data/linux-4.1.8/arc...nclude/asm/pgtable.h\r"
+    ],
+    [
+      0.200304,
+      " 97.31 MB O  43.50 MB C  43.42 MB D data/linux-4.1.8/arc...mach-dreamcast/irq.c\r"
+    ],
+    [
+      0.200328,
+      " 99.46 MB O  44.41 MB C  44.33 MB D data/linux-4.1.8/arc...artner-jet-setup.txt\r"
+    ],
+    [
+      0.200102,
+      "101.28 MB O  45.25 MB C  45.16 MB D data/linux-4.1.8/arc...rc/include/asm/ecc.h\r"
+    ],
+    [
+      0.200253,
+      "103.53 MB O  46.27 MB C  46.19 MB D data/linux-4.1.8/arc.../kernel/una_asm_64.S\r"
+    ],
+    [
+      0.200503,
+      "105.76 MB O  47.32 MB C  47.23 MB D data/linux-4.1.8/arch/tile/kernel/reboot.c \r"
+    ],
+    [
+      0.201177,
+      "107.64 MB O  48.27 MB C  48.18 MB D data/linux-4.1.8/arc...t/compressed/eboot.c\r"
+    ],
+    [
+      0.200192,
+      "109.82 MB O  49.22 MB C  49.13 MB D data/linux-4.1.8/arc...clude/asm/spinlock.h\r"
+    ],
+    [
+      0.200851,
+      "112.71 MB O  50.56 MB C  50.48 MB D data/linux-4.1.8/arch/x86/kernel/ptrace.c  \r"
+    ],
+    [
+      0.200195,
+      "115.71 MB O  51.96 MB C  51.87 MB D data/linux-4.1.8/arc...s/platform_emc1403.c\r"
+    ],
+    [
+      0.200306,
+      "117.28 MB O  52.79 MB C  52.70 MB D data/linux-4.1.8/arc...nclude/variant/tie.h\r"
+    ],
+    [
+      0.204475,
+      "122.68 MB O  55.35 MB C  55.26 MB D data/linux-4.1.8/fir...x-e1-6.2.9.0.fw.ihex\r"
+    ],
+    [
+      0.199974,
+      "127.39 MB O  58.15 MB C  57.97 MB D data/linux-4.1.8/fs/afs/fsclient.c         \r"
+    ],
+    [
+      0.201254,
+      "132.58 MB O  60.42 MB C  60.24 MB D data/linux-4.1.8/fs/cifs/cifssmb.c         \r"
+    ],
+    [
+      0.216710,
+      "136.76 MB O  62.28 MB C  62.10 MB D data/linux-4.1.8/fs/ext4/inline.c          \r"
+    ],
+    [
+      0.200891,
+      "140.78 MB O  64.15 MB C  63.97 MB D data/linux-4.1.8/fs/jbd2/commit.c          \r"
+    ],
+    [
+      0.199883,
+      "144.88 MB O  65.98 MB C  65.80 MB D data/linux-4.1.8/fs/nfs/objlayout          \r"
+    ],
+    [
+      0.201488,
+      "150.31 MB O  67.96 MB C  67.78 MB D data/linux-4.1.8/fs/...fy/dnotify/dnotify.c\r"
+    ],
+    [
+      0.205472,
+      "154.72 MB O  69.97 MB C  69.79 MB D data/linux-4.1.8/fs/quota/dquot.c          \r"
+    ],
+    [
+      0.200493,
+      "159.06 MB O  71.91 MB C  71.73 MB D data/linux-4.1.8/fs/...xfs/xfs_inode_fork.h\r"
+    ],
+    [
+      0.200000,
+      "161.54 MB O  73.09 MB C  72.91 MB D data/linux-4.1.8/inc.../crypto/public_key.h\r"
+    ],
+    [
+      0.205041,
+      "164.32 MB O  74.28 MB C  74.09 MB D data/linux-4.1.8/inc...inux/cgroup_subsys.h\r"
+    ],
+    [
+      0.200371,
+      "166.33 MB O  75.23 MB C  75.05 MB D data/linux-4.1.8/include/linux/if_team.h   \r"
+    ],
+    [
+      0.200340,
+      "168.82 MB O  76.24 MB C  76.06 MB D data/linux-4.1.8/inc.../mfd/pcf50633/gpio.h\r"
+    ],
+    [
+      0.200162,
+      "171.65 MB O  77.36 MB C  77.17 MB D data/linux-4.1.8/include/linux/phy.h       \r"
+    ],
+    [
+      0.200385,
+      "172.84 MB O  77.97 MB C  77.79 MB D data/linux-4.1.8/include/linux/scc.h       \r"
+    ],
+    [
+      0.200918,
+      "174.87 MB O  78.94 MB C  78.76 MB D data/linux-4.1.8/include/linux/wait.h      \r"
+    ],
+    [
+      0.200117,
+      "177.06 MB O  80.01 MB C  79.83 MB D data/linux-4.1.8/inc...er/nfnetlink_queue.h\r"
+    ],
+    [
+      0.200254,
+      "179.53 MB O  81.13 MB C  80.95 MB D data/linux-4.1.8/inc...e/events/intel-sst.h\r"
+    ],
+    [
+      0.200176,
+      "181.40 MB O  82.05 MB C  81.86 MB D data/linux-4.1.8/include/uapi/linux/mpls.h \r"
+    ],
+    [
+      0.200438,
+      "183.11 MB O  82.88 MB C  82.70 MB D data/linux-4.1.8/inc...api/scsi/fc/fc_els.h\r"
+    ],
+    [
+      0.200226,
+      "186.12 MB O  84.31 MB C  84.12 MB D data/linux-4.1.8/kernel/jump_label.c       \r"
+    ],
+    [
+      0.200138,
+      "190.76 MB O  86.46 MB C  86.28 MB D data/linux-4.1.8/lib/Kconfig.debug         \r"
+    ],
+    [
+      0.200958,
+      "194.21 MB O  87.82 MB C  87.64 MB D data/linux-4.1.8/mm/memblock.c             \r"
+    ],
+    [
+      0.200544,
+      "198.19 MB O  89.69 MB C  89.51 MB D data/linux-4.1.8/net/bluetooth/ecc.c       \r"
+    ],
+    [
+      0.200232,
+      "202.28 MB O  91.52 MB C  91.34 MB D data/linux-4.1.8/net/hsr/hsr_slave.c       \r"
+    ],
+    [
+      0.200153,
+      "206.23 MB O  93.40 MB C  93.22 MB D data/linux-4.1.8/net/ipx/af_ipx.c          \r"
+    ],
+    [
+      0.200526,
+      "210.30 MB O  95.08 MB C  94.89 MB D data/linux-4.1.8/net...ter/ipvs/ip_vs_ftp.c\r"
+    ],
+    [
+      0.200433,
+      "213.29 MB O  96.37 MB C  96.19 MB D data/linux-4.1.8/net/phonet/af_phonet.c    \r"
+    ],
+    [
+      0.200669,
+      "217.21 MB O  98.21 MB C  98.03 MB D data/linux-4.1.8/net.../svc_rdma_recvfrom.c\r"
+    ],
+    [
+      0.200014,
+      "220.20 MB O  99.53 MB C  99.35 MB D data/linux-4.1.8/scr...e/free/iounmap.cocci\r"
+    ],
+    [
+      0.200446,
+      "222.94 MB O 100.82 MB C 100.64 MB D data/linux-4.1.8/security/selinux/Makefile \r"
+    ],
+    [
+      0.214711,
+      "226.41 MB O 102.23 MB C 102.05 MB D data/linux-4.1.8/sou...seq/seq_midi_event.c\r"
+    ],
+    [
+      0.202631,
+      "228.96 MB O 103.31 MB C 103.13 MB D data/linux-4.1.8/sound/mips/ad1843.c       \r"
+    ],
+    [
+      0.200095,
+      "232.28 MB O 104.65 MB C 104.47 MB D data/linux-4.1.8/sound/pci/ctxfi/Makefile  \r"
+    ],
+    [
+      0.200726,
+      "236.33 MB O 106.24 MB C 106.06 MB D data/linux-4.1.8/sound/pci/nm256/Makefile  \r"
+    ],
+    [
+      0.199902,
+      "239.73 MB O 107.58 MB C 107.40 MB D data/linux-4.1.8/sou.../codecs/cs4271-spi.c\r"
+    ],
+    [
+      0.200592,
+      "244.29 MB O 109.08 MB C 108.90 MB D data/linux-4.1.8/sound/soc/codecs/wm8940.c \r"
+    ],
+    [
+      0.200357,
+      "247.98 MB O 110.35 MB C 110.17 MB D data/linux-4.1.8/sou...oc/omap/omap-mcpdm.c\r"
+    ],
+    [
+      0.200901,
+      "250.64 MB O 111.50 MB C 111.32 MB D data/linux-4.1.8/sound/usb/mixer_scarlett.c\r"
+    ],
+    [
+      0.200535,
+      "252.14 MB O 112.20 MB C 112.01 MB D data/linux-4.1.8/tools/perf/builtin-kvm.c  \r"
+    ],
+    [
+      0.200239,
+      "254.11 MB O 113.07 MB C 112.88 MB D data/linux-4.1.8/tools/perf/util/record.c  \r"
+    ],
+    [
+      0.200233,
+      "255.70 MB O 113.82 MB C 113.64 MB D data/linux-4.1.8/too...re/bin/configinit.sh\r"
+    ],
+    [
+      0.395702,
+      "                                                                               \r------------------------------------------------------------------------------\r\nArchive name: backup1\r\nArchive fingerprint: b3104802be9faa610f281619c69e4d3e672df2ce97528a35d83f15080d02ed86\r\nStart time: Sat Oct 24 22:27:24 2015\r\nEnd time: Sat Oct 24 22:27:43 2015\r\nDuration: 19.32 seconds\r\nNumber of files: 31557\r\n\r\n                       Original size      Compressed size    Deduplicated size\r\nThis archive:              257.06 MB            114.44 MB            114.26 MB\r\nAll archives:              257.06 MB            114.44 MB            114.26 MB\r\n\r\n                       Unique chunks         Total chunks\r\nChunk index:                   33731                34030\r\n------------------------------------------------------------------------------\r\n"
+    ],
+    [
+      0.046138,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      1.000000,
+      "#"
+    ],
+    [
+      0.564664,
+      " "
+    ],
+    [
+      0.313339,
+      "c"
+    ],
+    [
+      0.492152,
+      "h"
+    ],
+    [
+      0.479518,
+      "a"
+    ],
+    [
+      0.536708,
+      "n"
+    ],
+    [
+      0.134006,
+      "g"
+    ],
+    [
+      0.147326,
+      "e"
+    ],
+    [
+      0.068957,
+      " "
+    ],
+    [
+      0.179678,
+      "s"
+    ],
+    [
+      0.096249,
+      "o"
+    ],
+    [
+      0.081003,
+      "m"
+    ],
+    [
+      0.124342,
+      "e"
+    ],
+    [
+      0.117830,
+      " "
+    ],
+    [
+      0.138019,
+      "d"
+    ],
+    [
+      0.137898,
+      "a"
+    ],
+    [
+      0.199628,
+      "t"
+    ],
+    [
+      0.104935,
+      "a"
+    ],
+    [
+      0.150868,
+      " "
+    ],
+    [
+      0.144877,
+      "s"
+    ],
+    [
+      0.126816,
+      "l"
+    ],
+    [
+      0.178466,
+      "i"
+    ],
+    [
+      0.113395,
+      "g"
+    ],
+    [
+      0.101022,
+      "h"
+    ],
+    [
+      0.102395,
+      "t"
+    ],
+    [
+      0.311498,
+      "l"
+    ],
+    [
+      0.366608,
+      "y"
+    ],
+    [
+      0.657991,
+      ":"
+    ],
+    [
+      0.423140,
+      "\r\n"
+    ],
+    [
+      0.000708,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      2.000000,
+      "e"
+    ],
+    [
+      0.000021,
+      "c"
+    ],
+    [
+      0.000024,
+      "h"
+    ],
+    [
+      0.000029,
+      "o"
+    ],
+    [
+      0.000018,
+      " "
+    ],
+    [
+      0.000025,
+      "\""
+    ],
+    [
+      0.000025,
+      "s"
+    ],
+    [
+      0.000026,
+      "o"
+    ],
+    [
+      0.000070,
+      "me "
+    ],
+    [
+      0.000022,
+      "m"
+    ],
+    [
+      0.000028,
+      "o"
+    ],
+    [
+      0.000027,
+      "r"
+    ],
+    [
+      0.000026,
+      "e"
+    ],
+    [
+      0.000029,
+      " "
+    ],
+    [
+      0.000028,
+      "d"
+    ],
+    [
+      0.000028,
+      "a"
+    ],
+    [
+      0.000028,
+      "t"
+    ],
+    [
+      0.000026,
+      "a"
+    ],
+    [
+      0.000033,
+      "\""
+    ],
+    [
+      0.000028,
+      " "
+    ],
+    [
+      0.000059,
+      "\u003e"
+    ],
+    [
+      0.000045,
+      " "
+    ],
+    [
+      0.000020,
+      "d"
+    ],
+    [
+      0.000040,
+      "a"
+    ],
+    [
+      0.000035,
+      "t"
+    ],
+    [
+      0.000039,
+      "a"
+    ],
+    [
+      0.000034,
+      "/"
+    ],
+    [
+      0.000034,
+      "o"
+    ],
+    [
+      0.000034,
+      "n"
+    ],
+    [
+      0.000037,
+      "e"
+    ],
+    [
+      0.000036,
+      "_"
+    ],
+    [
+      0.000037,
+      "f"
+    ],
+    [
+      0.000037,
+      "i"
+    ],
+    [
+      0.000717,
+      "le_more\r\n"
+    ],
+    [
+      0.000181,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      2.000000,
+      "#"
+    ],
+    [
+      0.266289,
+      " "
+    ],
+    [
+      0.194686,
+      "n"
+    ],
+    [
+      0.157296,
+      "o"
+    ],
+    [
+      0.084026,
+      "w"
+    ],
+    [
+      0.092729,
+      " "
+    ],
+    [
+      0.148154,
+      "c"
+    ],
+    [
+      0.169136,
+      "r"
+    ],
+    [
+      0.214327,
+      "e"
+    ],
+    [
+      0.180678,
+      "a"
+    ],
+    [
+      0.161652,
+      "t"
+    ],
+    [
+      0.128260,
+      "e"
+    ],
+    [
+      0.158131,
+      " "
+    ],
+    [
+      0.118838,
+      "a"
+    ],
+    [
+      0.120885,
+      " "
+    ],
+    [
+      0.797511,
+      "s"
+    ],
+    [
+      0.200585,
+      "e"
+    ],
+    [
+      0.171811,
+      "c"
+    ],
+    [
+      0.106721,
+      "o"
+    ],
+    [
+      0.153298,
+      "n"
+    ],
+    [
+      0.052244,
+      "d"
+    ],
+    [
+      0.149675,
+      " "
+    ],
+    [
+      0.183517,
+      "b"
+    ],
+    [
+      0.076768,
+      "a"
+    ],
+    [
+      0.189428,
+      "c"
+    ],
+    [
+      0.088431,
+      "k"
+    ],
+    [
+      0.229617,
+      "u"
+    ],
+    [
+      0.272021,
+      "p"
+    ],
+    [
+      0.965855,
+      ":"
+    ],
+    [
+      0.674517,
+      "\r\n"
+    ],
+    [
+      0.000322,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.946131,
+      "b"
+    ],
+    [
+      0.111159,
+      "o"
+    ],
+    [
+      0.094622,
+      "r"
+    ],
+    [
+      0.085288,
+      "g"
+    ],
+    [
+      0.165429,
+      " "
+    ],
+    [
+      0.936087,
+      "c"
+    ],
+    [
+      0.192608,
+      "r"
+    ],
+    [
+      0.187511,
+      "e"
+    ],
+    [
+      0.173135,
+      "a"
+    ],
+    [
+      0.179441,
+      "t"
+    ],
+    [
+      0.125923,
+      "e"
+    ],
+    [
+      0.164920,
+      " "
+    ],
+    [
+      0.737259,
+      "-"
+    ],
+    [
+      0.185417,
+      "-"
+    ],
+    [
+      0.233405,
+      "s"
+    ],
+    [
+      0.152945,
+      "t"
+    ],
+    [
+      0.181548,
+      "a"
+    ],
+    [
+      0.330237,
+      "t"
+    ],
+    [
+      0.735524,
+      "s"
+    ],
+    [
+      0.179019,
+      " "
+    ],
+    [
+      0.245324,
+      "-"
+    ],
+    [
+      0.142362,
+      "-"
+    ],
+    [
+      0.233989,
+      "p"
+    ],
+    [
+      0.153782,
+      "r"
+    ],
+    [
+      0.064431,
+      "o"
+    ],
+    [
+      0.104827,
+      "g"
+    ],
+    [
+      0.090533,
+      "r"
+    ],
+    [
+      0.168129,
+      "e"
+    ],
+    [
+      0.206325,
+      "s"
+    ],
+    [
+      0.157551,
+      "s"
+    ],
+    [
+      0.383630,
+      " "
+    ],
+    [
+      0.759364,
+      "r"
+    ],
+    [
+      0.199262,
+      "e"
+    ],
+    [
+      0.139781,
+      "p"
+    ],
+    [
+      0.151367,
+      "o"
+    ],
+    [
+      0.720350,
+      ":"
+    ],
+    [
+      0.144801,
+      ":"
+    ],
+    [
+      0.532566,
+      "b"
+    ],
+    [
+      0.226514,
+      "a"
+    ],
+    [
+      0.209449,
+      "c"
+    ],
+    [
+      0.142062,
+      "k"
+    ],
+    [
+      0.300090,
+      "u"
+    ],
+    [
+      0.262794,
+      "p"
+    ],
+    [
+      0.218785,
+      "2"
+    ],
+    [
+      0.249599,
+      " "
+    ],
+    [
+      0.187125,
+      "d"
+    ],
+    [
+      0.157741,
+      "a"
+    ],
+    [
+      0.175739,
+      "t"
+    ],
+    [
+      0.139896,
+      "a"
+    ],
+    [
+      0.795560,
+      "\r\n"
+    ],
+    [
+      0.571808,
+      "  6.50 MB O   3.09 MB C       0 B D data/linux-4.1.8/Doc...ngs/arm/armadeus.txt\r"
+    ],
+    [
+      0.200103,
+      " 11.82 MB O   6.17 MB C       0 B D data/linux-4.1.8/Documentation/hwmon/w83795\r"
+    ],
+    [
+      0.200121,
+      " 27.38 MB O  13.89 MB C       0 B D data/linux-4.1.8/arc...ot/dts/nspire-cx.dts\r"
+    ],
+    [
+      0.200110,
+      " 39.92 MB O  19.04 MB C       0 B D data/linux-4.1.8/arc...omap2/opp2430_data.c\r"
+    ],
+    [
+      0.200088,
+      " 52.28 MB O  24.23 MB C       0 B D data/linux-4.1.8/arc...fin/mach-bf561/smp.c\r"
+    ],
+    [
+      0.200078,
+      " 67.02 MB O  29.94 MB C       0 B D data/linux-4.1.8/arc...8k/include/asm/pci.h\r"
+    ],
+    [
+      0.200116,
+      " 78.29 MB O  34.52 MB C       0 B D data/linux-4.1.8/arc...etlogic/xlr/wakeup.c\r"
+    ],
+    [
+      0.200081,
+      " 90.07 MB O  40.11 MB C       0 B D data/linux-4.1.8/arc...eature-fixups-test.S\r"
+    ],
+    [
+      0.200092,
+      "101.15 MB O  45.19 MB C       0 B D data/linux-4.1.8/arc...rc/crypto/sha1_asm.S\r"
+    ],
+    [
+      0.200078,
+      "115.05 MB O  51.63 MB C       0 B D data/linux-4.1.8/arc...6/mm/kasan_init_64.c\r"
+    ],
+    [
+      0.200062,
+      "147.39 MB O  66.98 MB C       0 B D data/linux-4.1.8/fs/nls/nls_cp864.c        \r"
+    ],
+    [
+      0.200117,
+      "169.16 MB O  76.38 MB C       0 B D data/linux-4.1.8/inc.../mfd/twl4030-audio.h\r"
+    ],
+    [
+      0.200074,
+      "181.43 MB O  82.06 MB C       0 B D data/linux-4.1.8/include/uapi/linux/mtio.h \r"
+    ],
+    [
+      0.200131,
+      "209.10 MB O  94.58 MB C       0 B D data/linux-4.1.8/net/mac80211/scan.c       \r"
+    ],
+    [
+      0.200079,
+      "234.87 MB O 105.68 MB C       0 B D data/linux-4.1.8/sou...i/hda/patch_si3054.c\r"
+    ],
+    [
+      0.200110,
+      "255.66 MB O 113.80 MB C       0 B D data/linux-4.1.8/too...ves/asm/asm-compat.h\r"
+    ],
+    [
+      0.201350,
+      "                                                                               \r------------------------------------------------------------------------------\r\nArchive name: backup2\r\nArchive fingerprint: 5737afe8ad2cda7667973b7f2e1d83f097ef3117b5753a38ba7664b616fbdc5a\r\nStart time: Sat Oct 24 22:28:24 2015\r\nEnd time: Sat Oct 24 22:28:27 2015\r\nDuration: 3.41 seconds\r\nNumber of files: 31557\r\n"
+    ],
+    [
+      0.001858,
+      "\r\n                       Original size      Compressed size    Deduplicated size\r\nThis archive:              257.06 MB            114.47 MB             45.19 kB\r\nAll archives:              514.12 MB            228.92 MB            114.31 MB\r\n\r\n                       Unique chunks         Total chunks\r\nChunk index:                   33733                68060\r\n------------------------------------------------------------------------------\r\n"
+    ],
+    [
+      0.033369,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      1.013315,
+      "#"
+    ],
+    [
+      0.482095,
+      " "
+    ],
+    [
+      0.319571,
+      "w"
+    ],
+    [
+      0.140740,
+      "o"
+    ],
+    [
+      0.090380,
+      "w"
+    ],
+    [
+      0.304400,
+      ","
+    ],
+    [
+      0.137310,
+      " "
+    ],
+    [
+      0.662280,
+      "t"
+    ],
+    [
+      0.162678,
+      "h"
+    ],
+    [
+      0.114083,
+      "a"
+    ],
+    [
+      0.077660,
+      "t"
+    ],
+    [
+      0.120839,
+      " "
+    ],
+    [
+      0.207626,
+      "w"
+    ],
+    [
+      0.195480,
+      "a"
+    ],
+    [
+      0.060188,
+      "s"
+    ],
+    [
+      0.149129,
+      " "
+    ],
+    [
+      0.094522,
+      "a"
+    ],
+    [
+      0.098801,
+      " "
+    ],
+    [
+      0.266235,
+      "l"
+    ],
+    [
+      0.184774,
+      "o"
+    ],
+    [
+      0.255040,
+      "t"
+    ],
+    [
+      0.170498,
+      " "
+    ],
+    [
+      0.201599,
+      "f"
+    ],
+    [
+      0.189774,
+      "a"
+    ],
+    [
+      0.229140,
+      "s"
+    ],
+    [
+      0.275243,
+      "t"
+    ],
+    [
+      0.177347,
+      "e"
+    ],
+    [
+      0.090806,
+      "r"
+    ],
+    [
+      0.204494,
+      "!"
+    ],
+    [
+      0.479851,
+      "\r\n"
+    ],
+    [
+      0.000316,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.734961,
+      "#"
+    ],
+    [
+      0.300000,
+      " "
+    ],
+    [
+      0.300000,
+      "n"
+    ],
+    [
+      0.199297,
+      "o"
+    ],
+    [
+      0.148047,
+      "t"
+    ],
+    [
+      0.071101,
+      "i"
+    ],
+    [
+      0.185554,
+      "c"
+    ],
+    [
+      0.395933,
+      "e"
+    ],
+    [
+      0.180285,
+      " "
+    ],
+    [
+      0.199321,
+      "t"
+    ],
+    [
+      0.094767,
+      "h"
+    ],
+    [
+      0.166966,
+      "a"
+    ],
+    [
+      0.102814,
+      "t"
+    ],
+    [
+      0.415016,
+      " "
+    ],
+    [
+      0.286089,
+      "\""
+    ],
+    [
+      0.795323,
+      "D"
+    ],
+    [
+      0.180152,
+      "e"
+    ],
+    [
+      0.311214,
+      "d"
+    ],
+    [
+      0.214812,
+      "u"
+    ],
+    [
+      0.251616,
+      "p"
+    ],
+    [
+      0.203533,
+      "l"
+    ],
+    [
+      0.187084,
+      "i"
+    ],
+    [
+      0.124066,
+      "c"
+    ],
+    [
+      0.158062,
+      "a"
+    ],
+    [
+      0.260540,
+      "t"
+    ],
+    [
+      0.136405,
+      "e"
+    ],
+    [
+      0.278039,
+      "d"
+    ],
+    [
+      0.323148,
+      " "
+    ],
+    [
+      0.172337,
+      "s"
+    ],
+    [
+      0.074541,
+      "i"
+    ],
+    [
+      0.269245,
+      "z"
+    ],
+    [
+      0.123599,
+      "e"
+    ],
+    [
+      0.533647,
+      "\""
+    ],
+    [
+      0.234738,
+      " "
+    ],
+    [
+      0.150720,
+      "f"
+    ],
+    [
+      0.144329,
+      "o"
+    ],
+    [
+      0.086533,
+      "r"
+    ],
+    [
+      0.159717,
+      " "
+    ],
+    [
+      0.274291,
+      "\""
+    ],
+    [
+      0.471163,
+      "T"
+    ],
+    [
+      0.162135,
+      "h"
+    ],
+    [
+      0.233501,
+      "i"
+    ],
+    [
+      0.134923,
+      "s"
+    ],
+    [
+      0.190779,
+      " "
+    ],
+    [
+      0.307322,
+      "a"
+    ],
+    [
+      0.153882,
+      "r"
+    ],
+    [
+      0.246471,
+      "c"
+    ],
+    [
+      0.110018,
+      "h"
+    ],
+    [
+      0.259798,
+      "i"
+    ],
+    [
+      0.132853,
+      "v"
+    ],
+    [
+      0.171373,
+      "e"
+    ],
+    [
+      0.560405,
+      "\""
+    ],
+    [
+      0.609162,
+      "!"
+    ],
+    [
+      0.559020,
+      "\r\n"
+    ],
+    [
+      0.000296,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.941369,
+      "#"
+    ],
+    [
+      0.254237,
+      " "
+    ],
+    [
+      0.176998,
+      "b"
+    ],
+    [
+      0.124943,
+      "o"
+    ],
+    [
+      0.097140,
+      "r"
+    ],
+    [
+      0.057513,
+      "g"
+    ],
+    [
+      0.232990,
+      " "
+    ],
+    [
+      0.549539,
+      "r"
+    ],
+    [
+      0.112992,
+      "e"
+    ],
+    [
+      0.240733,
+      "c"
+    ],
+    [
+      0.164146,
+      "o"
+    ],
+    [
+      0.209755,
+      "g"
+    ],
+    [
+      0.145638,
+      "n"
+    ],
+    [
+      0.151826,
+      "i"
+    ],
+    [
+      0.471625,
+      "z"
+    ],
+    [
+      0.759625,
+      "e"
+    ],
+    [
+      0.229566,
+      "d"
+    ],
+    [
+      0.254596,
+      " "
+    ],
+    [
+      0.209452,
+      "t"
+    ],
+    [
+      0.088606,
+      "h"
+    ],
+    [
+      0.155981,
+      "a"
+    ],
+    [
+      0.086797,
+      "t"
+    ],
+    [
+      0.098574,
+      " "
+    ],
+    [
+      0.243290,
+      "m"
+    ],
+    [
+      0.120288,
+      "o"
+    ],
+    [
+      0.092890,
+      "s"
+    ],
+    [
+      0.058823,
+      "t"
+    ],
+    [
+      0.125344,
+      " "
+    ],
+    [
+      0.211464,
+      "f"
+    ],
+    [
+      0.086483,
+      "i"
+    ],
+    [
+      0.213685,
+      "l"
+    ],
+    [
+      0.096764,
+      "e"
+    ],
+    [
+      0.176075,
+      "s"
+    ],
+    [
+      0.122962,
+      " "
+    ],
+    [
+      0.174342,
+      "d"
+    ],
+    [
+      0.103474,
+      "i"
+    ],
+    [
+      0.089744,
+      "d"
+    ],
+    [
+      0.181539,
+      " "
+    ],
+    [
+      0.461771,
+      "n"
+    ],
+    [
+      0.219395,
+      "o"
+    ],
+    [
+      0.095042,
+      "t"
+    ],
+    [
+      0.119662,
+      " "
+    ],
+    [
+      0.156060,
+      "c"
+    ],
+    [
+      0.116988,
+      "h"
+    ],
+    [
+      0.118775,
+      "a"
+    ],
+    [
+      0.126173,
+      "n"
+    ],
+    [
+      0.118518,
+      "g"
+    ],
+    [
+      0.109977,
+      "e"
+    ],
+    [
+      0.167095,
+      " "
+    ],
+    [
+      0.208137,
+      "a"
+    ],
+    [
+      0.155464,
+      "n"
+    ],
+    [
+      0.074939,
+      "d"
+    ],
+    [
+      0.616534,
+      "\r\n"
+    ],
+    [
+      0.000405,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.836535,
+      "#"
+    ],
+    [
+      0.248630,
+      " "
+    ],
+    [
+      0.211525,
+      "d"
+    ],
+    [
+      0.171252,
+      "e"
+    ],
+    [
+      0.244098,
+      "d"
+    ],
+    [
+      0.121718,
+      "u"
+    ],
+    [
+      0.219002,
+      "p"
+    ],
+    [
+      0.197839,
+      "l"
+    ],
+    [
+      0.161081,
+      "i"
+    ],
+    [
+      0.112763,
+      "c"
+    ],
+    [
+      0.154565,
+      "a"
+    ],
+    [
+      0.230427,
+      "t"
+    ],
+    [
+      0.180004,
+      "e"
+    ],
+    [
+      0.182279,
+      "d"
+    ],
+    [
+      0.201281,
+      " "
+    ],
+    [
+      0.202485,
+      "t"
+    ],
+    [
+      0.078397,
+      "h"
+    ],
+    [
+      0.178577,
+      "e"
+    ],
+    [
+      0.150264,
+      "m"
+    ],
+    [
+      0.482274,
+      "."
+    ],
+    [
+      0.300000,
+      "\r\n"
+    ],
+    [
+      0.000265,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.287124,
+      "#"
+    ],
+    [
+      0.314208,
+      " "
+    ],
+    [
+      0.219731,
+      "n"
+    ],
+    [
+      0.176210,
+      "o"
+    ],
+    [
+      0.108529,
+      "w"
+    ],
+    [
+      0.224056,
+      ","
+    ],
+    [
+      0.210976,
+      " "
+    ],
+    [
+      0.190508,
+      "l"
+    ],
+    [
+      0.098452,
+      "e"
+    ],
+    [
+      0.101431,
+      "t"
+    ],
+    [
+      0.855722,
+      "'"
+    ],
+    [
+      0.220403,
+      "s"
+    ],
+    [
+      0.229447,
+      " "
+    ],
+    [
+      0.134839,
+      "e"
+    ],
+    [
+      0.241915,
+      "x"
+    ],
+    [
+      0.217004,
+      "t"
+    ],
+    [
+      0.183774,
+      "r"
+    ],
+    [
+      0.231721,
+      "a"
+    ],
+    [
+      0.221361,
+      "c"
+    ],
+    [
+      0.436221,
+      "t"
+    ],
+    [
+      0.097256,
+      " "
+    ],
+    [
+      0.163933,
+      "a"
+    ],
+    [
+      0.099964,
+      " "
+    ],
+    [
+      0.216806,
+      "b"
+    ],
+    [
+      0.086493,
+      "a"
+    ],
+    [
+      0.211732,
+      "c"
+    ],
+    [
+      0.139016,
+      "k"
+    ],
+    [
+      0.379423,
+      "u"
+    ],
+    [
+      0.250049,
+      "p"
+    ],
+    [
+      0.717916,
+      ":"
+    ],
+    [
+      0.307136,
+      "\r\n"
+    ],
+    [
+      0.000301,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.967533,
+      "m"
+    ],
+    [
+      0.103707,
+      "v"
+    ],
+    [
+      0.407957,
+      " "
+    ],
+    [
+      0.177312,
+      "d"
+    ],
+    [
+      0.166158,
+      "a"
+    ],
+    [
+      0.242593,
+      "t"
+    ],
+    [
+      0.090471,
+      "a"
+    ],
+    [
+      0.699594,
+      " "
+    ],
+    [
+      0.273219,
+      "d"
+    ],
+    [
+      0.170371,
+      "a"
+    ],
+    [
+      0.169331,
+      "t"
+    ],
+    [
+      0.126739,
+      "a"
+    ],
+    [
+      0.288488,
+      "."
+    ],
+    [
+      0.305856,
+      "o"
+    ],
+    [
+      0.135252,
+      "r"
+    ],
+    [
+      0.152717,
+      "i"
+    ],
+    [
+      0.090343,
+      "g"
+    ],
+    [
+      0.312536,
+      "\r\n"
+    ],
+    [
+      0.002579,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.944876,
+      "b"
+    ],
+    [
+      0.149049,
+      "o"
+    ],
+    [
+      0.114834,
+      "r"
+    ],
+    [
+      0.074682,
+      "g"
+    ],
+    [
+      0.129000,
+      " "
+    ],
+    [
+      0.129618,
+      "e"
+    ],
+    [
+      0.261479,
+      "x"
+    ],
+    [
+      0.203937,
+      "t"
+    ],
+    [
+      0.196213,
+      "r"
+    ],
+    [
+      0.193561,
+      "a"
+    ],
+    [
+      0.215314,
+      "c"
+    ],
+    [
+      0.236817,
+      "t"
+    ],
+    [
+      0.188232,
+      " "
+    ],
+    [
+      0.177286,
+      "r"
+    ],
+    [
+      0.200598,
+      "e"
+    ],
+    [
+      0.105866,
+      "p"
+    ],
+    [
+      0.173864,
+      "o"
+    ],
+    [
+      0.388954,
+      ":"
+    ],
+    [
+      0.144865,
+      ":"
+    ],
+    [
+      0.347420,
+      "b"
+    ],
+    [
+      0.105814,
+      "a"
+    ],
+    [
+      0.198728,
+      "c"
+    ],
+    [
+      0.096349,
+      "k"
+    ],
+    [
+      0.261559,
+      "u"
+    ],
+    [
+      0.241998,
+      "p"
+    ],
+    [
+      0.240033,
+      "2"
+    ],
+    [
+      0.981903,
+      "\r\n"
+    ],
+    [
+      2.000000,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.937291,
+      "#"
+    ],
+    [
+      0.994897,
+      " "
+    ],
+    [
+      0.151752,
+      "c"
+    ],
+    [
+      0.096956,
+      "h"
+    ],
+    [
+      0.337975,
+      "e"
+    ],
+    [
+      0.207037,
+      "c"
+    ],
+    [
+      0.177028,
+      "k"
+    ],
+    [
+      0.740370,
+      " "
+    ],
+    [
+      0.330206,
+      "i"
+    ],
+    [
+      0.177976,
+      "f"
+    ],
+    [
+      0.218757,
+      " "
+    ],
+    [
+      0.329345,
+      "o"
+    ],
+    [
+      0.098735,
+      "r"
+    ],
+    [
+      0.098576,
+      "i"
+    ],
+    [
+      0.103157,
+      "g"
+    ],
+    [
+      0.107275,
+      "i"
+    ],
+    [
+      0.117332,
+      "n"
+    ],
+    [
+      0.194072,
+      "a"
+    ],
+    [
+      0.211456,
+      "l"
+    ],
+    [
+      0.197712,
+      " "
+    ],
+    [
+      0.189172,
+      "d"
+    ],
+    [
+      0.163930,
+      "a"
+    ],
+    [
+      0.188334,
+      "t"
+    ],
+    [
+      0.165129,
+      "a"
+    ],
+    [
+      0.220652,
+      " "
+    ],
+    [
+      0.224411,
+      "a"
+    ],
+    [
+      0.136137,
+      "n"
+    ],
+    [
+      0.155260,
+      "d"
+    ],
+    [
+      0.074238,
+      " "
+    ],
+    [
+      0.104154,
+      "r"
+    ],
+    [
+      0.690499,
+      "e"
+    ],
+    [
+      0.193678,
+      "s"
+    ],
+    [
+      0.165163,
+      "t"
+    ],
+    [
+      0.165594,
+      "o"
+    ],
+    [
+      0.111779,
+      "r"
+    ],
+    [
+      0.135625,
+      "e"
+    ],
+    [
+      0.202851,
+      "d"
+    ],
+    [
+      0.096040,
+      " "
+    ],
+    [
+      0.165090,
+      "d"
+    ],
+    [
+      0.155594,
+      "a"
+    ],
+    [
+      0.220606,
+      "t"
+    ],
+    [
+      0.163143,
+      "a"
+    ],
+    [
+      0.174099,
+      " "
+    ],
+    [
+      0.209780,
+      "d"
+    ],
+    [
+      0.166062,
+      "i"
+    ],
+    [
+      0.084688,
+      "f"
+    ],
+    [
+      0.140851,
+      "f"
+    ],
+    [
+      0.204458,
+      "e"
+    ],
+    [
+      0.088661,
+      "r"
+    ],
+    [
+      0.334162,
+      "s"
+    ],
+    [
+      0.904233,
+      ":"
+    ],
+    [
+      0.590489,
+      "\r\n"
+    ],
+    [
+      0.000283,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.503183,
+      "d"
+    ],
+    [
+      0.082614,
+      "i"
+    ],
+    [
+      0.216272,
+      "f"
+    ],
+    [
+      0.123813,
+      "f"
+    ],
+    [
+      0.183603,
+      " "
+    ],
+    [
+      0.302144,
+      "-"
+    ],
+    [
+      0.150946,
+      "r"
+    ],
+    [
+      0.152436,
+      " "
+    ],
+    [
+      2.000000,
+      "d"
+    ],
+    [
+      0.196047,
+      "a"
+    ],
+    [
+      0.206372,
+      "t"
+    ],
+    [
+      0.146051,
+      "a"
+    ],
+    [
+      0.326306,
+      "."
+    ],
+    [
+      0.363408,
+      "o"
+    ],
+    [
+      0.269988,
+      "rig/"
+    ],
+    [
+      0.776581,
+      " "
+    ],
+    [
+      0.137720,
+      "d"
+    ],
+    [
+      0.156080,
+      "a"
+    ],
+    [
+      0.242275,
+      "\u0007"
+    ],
+    [
+      0.000020,
+      "ta"
+    ],
+    [
+      0.872887,
+      "/"
+    ],
+    [
+      0.273993,
+      "\r\n"
+    ],
+    [
+      2.000000,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.488581,
+      "#"
+    ],
+    [
+      0.234021,
+      " "
+    ],
+    [
+      0.380938,
+      "n"
+    ],
+    [
+      0.240685,
+      "o"
+    ],
+    [
+      0.204436,
+      " "
+    ],
+    [
+      0.390794,
+      "o"
+    ],
+    [
+      0.225563,
+      "u"
+    ],
+    [
+      0.167295,
+      "t"
+    ],
+    [
+      0.140625,
+      "p"
+    ],
+    [
+      0.183668,
+      "u"
+    ],
+    [
+      0.106161,
+      "t"
+    ],
+    [
+      0.132063,
+      " "
+    ],
+    [
+      0.204757,
+      "m"
+    ],
+    [
+      0.082693,
+      "e"
+    ],
+    [
+      0.216428,
+      "a"
+    ],
+    [
+      0.121584,
+      "n"
+    ],
+    [
+      0.127398,
+      "s"
+    ],
+    [
+      0.264644,
+      " "
+    ],
+    [
+      0.201524,
+      "i"
+    ],
+    [
+      0.110738,
+      "t"
+    ],
+    [
+      0.120653,
+      " "
+    ],
+    [
+      0.311187,
+      "d"
+    ],
+    [
+      0.119826,
+      "o"
+    ],
+    [
+      0.082654,
+      "e"
+    ],
+    [
+      0.182518,
+      "s"
+    ],
+    [
+      0.096372,
+      " "
+    ],
+    [
+      0.192821,
+      "n"
+    ],
+    [
+      0.193829,
+      "o"
+    ],
+    [
+      0.065739,
+      "t"
+    ],
+    [
+      0.678808,
+      "."
+    ],
+    [
+      0.246797,
+      " "
+    ],
+    [
+      0.520369,
+      "f"
+    ],
+    [
+      0.058288,
+      "i"
+    ],
+    [
+      0.064783,
+      "n"
+    ],
+    [
+      0.104851,
+      "e"
+    ],
+    [
+      0.292910,
+      "."
+    ],
+    [
+      0.174086,
+      " "
+    ],
+    [
+      0.226556,
+      ":"
+    ],
+    [
+      0.249808,
+      ")"
+    ],
+    [
+      2.000000,
+      "\r\n"
+    ],
+    [
+      0.000261,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.477759,
+      "#"
+    ],
+    [
+      0.223699,
+      " "
+    ],
+    [
+      0.237979,
+      "l"
+    ],
+    [
+      0.152769,
+      "i"
+    ],
+    [
+      0.135150,
+      "s"
+    ],
+    [
+      0.068576,
+      "t"
+    ],
+    [
+      0.100516,
+      "i"
+    ],
+    [
+      0.078648,
+      "n"
+    ],
+    [
+      0.099435,
+      "g"
+    ],
+    [
+      0.157388,
+      " "
+    ],
+    [
+      0.115327,
+      "t"
+    ],
+    [
+      0.133738,
+      "h"
+    ],
+    [
+      0.135662,
+      "e"
+    ],
+    [
+      0.100677,
+      " "
+    ],
+    [
+      0.180392,
+      "r"
+    ],
+    [
+      0.190922,
+      "e"
+    ],
+    [
+      0.093920,
+      "p"
+    ],
+    [
+      0.173588,
+      "o"
+    ],
+    [
+      0.193023,
+      " "
+    ],
+    [
+      0.206907,
+      "c"
+    ],
+    [
+      0.106376,
+      "o"
+    ],
+    [
+      0.175291,
+      "n"
+    ],
+    [
+      0.080726,
+      "t"
+    ],
+    [
+      0.179258,
+      "e"
+    ],
+    [
+      0.101491,
+      "n"
+    ],
+    [
+      0.096807,
+      "t"
+    ],
+    [
+      0.211455,
+      "s"
+    ],
+    [
+      0.508210,
+      ":"
+    ],
+    [
+      0.373837,
+      "\r\n"
+    ],
+    [
+      0.000249,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.559782,
+      "b"
+    ],
+    [
+      0.116587,
+      "o"
+    ],
+    [
+      0.139513,
+      "r"
+    ],
+    [
+      0.072751,
+      "g"
+    ],
+    [
+      0.103968,
+      " "
+    ],
+    [
+      0.984928,
+      "l"
+    ],
+    [
+      0.173603,
+      "i"
+    ],
+    [
+      0.112444,
+      "s"
+    ],
+    [
+      0.066704,
+      "t"
+    ],
+    [
+      0.114771,
+      " "
+    ],
+    [
+      0.263745,
+      "r"
+    ],
+    [
+      0.113121,
+      "e"
+    ],
+    [
+      0.126283,
+      "p"
+    ],
+    [
+      0.187453,
+      "o"
+    ],
+    [
+      0.409044,
+      "\r\n"
+    ],
+    [
+      0.360675,
+      "backup1                              Sat Oct 24 22:27:43 2015"
+    ],
+    [
+      0.000011,
+      "\r\n"
+    ],
+    [
+      0.000006,
+      "backup2                              Sat Oct 24 22:28:27 2015"
+    ],
+    [
+      0.000005,
+      "\r\n"
+    ],
+    [
+      0.027766,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.637813,
+      "#"
+    ],
+    [
+      0.257629,
+      " "
+    ],
+    [
+      0.231710,
+      "l"
+    ],
+    [
+      0.213387,
+      "i"
+    ],
+    [
+      0.132149,
+      "s"
+    ],
+    [
+      0.244957,
+      "t"
+    ],
+    [
+      0.180264,
+      "i"
+    ],
+    [
+      0.082882,
+      "n"
+    ],
+    [
+      0.142810,
+      "g"
+    ],
+    [
+      0.134815,
+      " "
+    ],
+    [
+      0.167455,
+      "s"
+    ],
+    [
+      0.114155,
+      "o"
+    ],
+    [
+      0.106847,
+      "m"
+    ],
+    [
+      0.070629,
+      "e"
+    ],
+    [
+      0.507340,
+      " "
+    ],
+    [
+      0.234237,
+      "b"
+    ],
+    [
+      0.070181,
+      "a"
+    ],
+    [
+      0.220534,
+      "c"
+    ],
+    [
+      0.092316,
+      "k"
+    ],
+    [
+      0.257003,
+      "u"
+    ],
+    [
+      0.233598,
+      "p"
+    ],
+    [
+      0.201484,
+      " "
+    ],
+    [
+      0.124810,
+      "a"
+    ],
+    [
+      0.084732,
+      "r"
+    ],
+    [
+      0.249719,
+      "c"
+    ],
+    [
+      0.119605,
+      "h"
+    ],
+    [
+      0.203875,
+      "i"
+    ],
+    [
+      0.076269,
+      "v"
+    ],
+    [
+      0.174299,
+      "e"
+    ],
+    [
+      0.109711,
+      " "
+    ],
+    [
+      0.238294,
+      "c"
+    ],
+    [
+      0.102351,
+      "o"
+    ],
+    [
+      0.155761,
+      "n"
+    ],
+    [
+      0.060278,
+      "t"
+    ],
+    [
+      0.179564,
+      "e"
+    ],
+    [
+      0.112342,
+      "n"
+    ],
+    [
+      0.078100,
+      "t"
+    ],
+    [
+      0.190203,
+      "s"
+    ],
+    [
+      0.865560,
+      " "
+    ],
+    [
+      0.297799,
+      "("
+    ],
+    [
+      0.225741,
+      "s"
+    ],
+    [
+      0.080329,
+      "h"
+    ],
+    [
+      0.233668,
+      "o"
+    ],
+    [
+      0.127773,
+      "r"
+    ],
+    [
+      0.190065,
+      "t"
+    ],
+    [
+      0.187679,
+      "e"
+    ],
+    [
+      0.147219,
+      "n"
+    ],
+    [
+      0.064472,
+      "e"
+    ],
+    [
+      0.188512,
+      "d"
+    ],
+    [
+      0.459222,
+      ")"
+    ],
+    [
+      0.723165,
+      ":"
+    ],
+    [
+      0.645995,
+      "\r\n"
+    ],
+    [
+      0.000096,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.446688,
+      "b"
+    ],
+    [
+      0.145841,
+      "o"
+    ],
+    [
+      0.105605,
+      "r"
+    ],
+    [
+      0.088953,
+      "g"
+    ],
+    [
+      0.120803,
+      " "
+    ],
+    [
+      0.227780,
+      "l"
+    ],
+    [
+      0.175052,
+      "i"
+    ],
+    [
+      0.106579,
+      "s"
+    ],
+    [
+      0.058441,
+      "t"
+    ],
+    [
+      0.093196,
+      " "
+    ],
+    [
+      0.172940,
+      "r"
+    ],
+    [
+      0.134731,
+      "e"
+    ],
+    [
+      0.119062,
+      "p"
+    ],
+    [
+      0.183075,
+      "o"
+    ],
+    [
+      0.388321,
+      ":"
+    ],
+    [
+      0.140589,
+      ":"
+    ],
+    [
+      0.324109,
+      "b"
+    ],
+    [
+      0.058606,
+      "a"
+    ],
+    [
+      0.205450,
+      "c"
+    ],
+    [
+      0.105362,
+      "k"
+    ],
+    [
+      0.235009,
+      "u"
+    ],
+    [
+      0.243485,
+      "p"
+    ],
+    [
+      0.485432,
+      "2"
+    ],
+    [
+      0.148177,
+      " "
+    ],
+    [
+      0.632383,
+      "|"
+    ],
+    [
+      0.389914,
+      " "
+    ],
+    [
+      0.174128,
+      "t"
+    ],
+    [
+      0.201473,
+      "a"
+    ],
+    [
+      0.116517,
+      "i"
+    ],
+    [
+      0.225072,
+      "l"
+    ],
+    [
+      0.699624,
+      "\r\n"
+    ],
+    [
+      2.000000,
+      "-rw-rw-r-- tw     tw         5516 Jul 21 19:10 data/linux-4.1.8/virt/kvm/async_pf.c\r\n"
+    ],
+    [
+      0.000019,
+      "-rw-rw-r-- tw     tw         1120 Jul 21 19:10 data/linux-4.1.8/virt/kvm/async_pf.h\r\n-rw-rw-r-- tw     tw         4215 Jul 21 19:10 data/linux-4.1.8/virt/kvm/coalesced_mmio.c\r\n-rw-rw-r-- tw     tw          915 Jul 21 19:10 data/linux-4.1.8/virt/kvm/coalesced_mmio.h\r\n-rw-rw-r-- tw     tw        22879 Jul 21 19:10 data/linux-4.1.8/virt/kvm/eventfd.c\r\n-rw-rw-r-- tw     tw         5563 Jul 21 19:10 data/linux-4.1.8/virt/kvm/irqchip.c\r\n-rw-rw-r-- tw     tw        79385 Jul 21 19:10 data/linux-4.1.8/virt/kvm/kvm_main.c\r\n"
+    ],
+    [
+      0.000011,
+      "-rw-rw-r-- tw     tw         6132 Jul 21 19:10 data/linux-4.1.8/virt/kvm/vfio.c\r\n-rw-rw-r-- tw     tw          250 Jul 21 19:10 data/linux-4.1.8/virt/kvm/vfio.h\r\n"
+    ],
+    [
+      0.000009,
+      "-rw-rw-r-- tw     tw           15 Oct 24 22:28 data/one_file_more\r\n"
+    ],
+    [
+      0.000389,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      1.000000,
+      "#"
+    ],
+    [
+      0.337351,
+      " "
+    ],
+    [
+      0.147170,
+      "e"
+    ],
+    [
+      0.235736,
+      "a"
+    ],
+    [
+      0.251314,
+      "s"
+    ],
+    [
+      0.471185,
+      "y"
+    ],
+    [
+      0.277723,
+      ","
+    ],
+    [
+      0.204225,
+      " "
+    ],
+    [
+      0.182231,
+      "i"
+    ],
+    [
+      0.174424,
+      "s"
+    ],
+    [
+      0.074677,
+      "n"
+    ],
+    [
+      0.786274,
+      "'"
+    ],
+    [
+      0.264836,
+      "t"
+    ],
+    [
+      0.330352,
+      " "
+    ],
+    [
+      0.266876,
+      "i"
+    ],
+    [
+      0.112564,
+      "t"
+    ],
+    [
+      0.897299,
+      "?"
+    ],
+    [
+      0.623501,
+      " "
+    ],
+    [
+      0.656625,
+      "t"
+    ],
+    [
+      0.115934,
+      "h"
+    ],
+    [
+      0.625213,
+      "a"
+    ],
+    [
+      0.588409,
+      "t"
+    ],
+    [
+      0.160071,
+      " "
+    ],
+    [
+      0.830693,
+      "i"
+    ],
+    [
+      0.163118,
+      "s"
+    ],
+    [
+      0.075663,
+      " "
+    ],
+    [
+      0.186138,
+      "a"
+    ],
+    [
+      0.109916,
+      "l"
+    ],
+    [
+      0.137005,
+      "l"
+    ],
+    [
+      0.171009,
+      " "
+    ],
+    [
+      0.153348,
+      "y"
+    ],
+    [
+      0.132919,
+      "o"
+    ],
+    [
+      0.568100,
+      "u"
+    ],
+    [
+      0.211350,
+      " "
+    ],
+    [
+      0.195450,
+      "n"
+    ],
+    [
+      0.257974,
+      "e"
+    ],
+    [
+      0.185529,
+      "e"
+    ],
+    [
+      0.265130,
+      "d"
+    ],
+    [
+      0.129116,
+      " "
+    ],
+    [
+      0.169264,
+      "t"
+    ],
+    [
+      0.148964,
+      "o"
+    ],
+    [
+      0.437043,
+      " "
+    ],
+    [
+      0.431197,
+      "k"
+    ],
+    [
+      0.219557,
+      "n"
+    ],
+    [
+      0.257996,
+      "o"
+    ],
+    [
+      0.158826,
+      "w"
+    ],
+    [
+      0.406870,
+      " "
+    ],
+    [
+      0.659664,
+      "f"
+    ],
+    [
+      0.130963,
+      "o"
+    ],
+    [
+      0.125395,
+      "r"
+    ],
+    [
+      0.613713,
+      " "
+    ],
+    [
+      0.646957,
+      "b"
+    ],
+    [
+      0.154695,
+      "a"
+    ],
+    [
+      0.259741,
+      "s"
+    ],
+    [
+      0.156692,
+      "i"
+    ],
+    [
+      0.124345,
+      "c"
+    ],
+    [
+      0.513209,
+      "\r\n"
+    ],
+    [
+      0.000296,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.965828,
+      "#"
+    ],
+    [
+      0.232285,
+      " "
+    ],
+    [
+      0.266818,
+      "u"
+    ],
+    [
+      0.132723,
+      "s"
+    ],
+    [
+      0.216208,
+      "a"
+    ],
+    [
+      0.206362,
+      "g"
+    ],
+    [
+      0.142608,
+      "e"
+    ],
+    [
+      2.000000,
+      "."
+    ],
+    [
+      0.238868,
+      " "
+    ],
+    [
+      0.302986,
+      "i"
+    ],
+    [
+      0.196338,
+      "f"
+    ],
+    [
+      0.092936,
+      " "
+    ],
+    [
+      0.197594,
+      "y"
+    ],
+    [
+      0.122297,
+      "o"
+    ],
+    [
+      0.175360,
+      "u"
+    ],
+    [
+      0.145063,
+      " "
+    ],
+    [
+      0.313719,
+      "l"
+    ],
+    [
+      0.169678,
+      "i"
+    ],
+    [
+      0.185628,
+      "k"
+    ],
+    [
+      0.120660,
+      "e"
+    ],
+    [
+      0.078389,
+      " "
+    ],
+    [
+      0.648628,
+      "#"
+    ],
+    [
+      0.337514,
+      "b"
+    ],
+    [
+      0.108598,
+      "o"
+    ],
+    [
+      0.123792,
+      "r"
+    ],
+    [
+      0.136099,
+      "g"
+    ],
+    [
+      0.235539,
+      "b"
+    ],
+    [
+      0.091671,
+      "a"
+    ],
+    [
+      0.208697,
+      "c"
+    ],
+    [
+      0.100567,
+      "k"
+    ],
+    [
+      0.227477,
+      "u"
+    ],
+    [
+      0.236900,
+      "p"
+    ],
+    [
+      0.302154,
+      ","
+    ],
+    [
+      0.207291,
+      " "
+    ],
+    [
+      0.205656,
+      "s"
+    ],
+    [
+      0.123737,
+      "p"
+    ],
+    [
+      0.142016,
+      "r"
+    ],
+    [
+      0.197260,
+      "e"
+    ],
+    [
+      0.197471,
+      "a"
+    ],
+    [
+      0.104498,
+      "d"
+    ],
+    [
+      0.163267,
+      " "
+    ],
+    [
+      0.178420,
+      "t"
+    ],
+    [
+      0.091669,
+      "h"
+    ],
+    [
+      0.107735,
+      "e"
+    ],
+    [
+      0.102742,
+      " "
+    ],
+    [
+      0.211413,
+      "w"
+    ],
+    [
+      0.124959,
+      "o"
+    ],
+    [
+      0.105787,
+      "r"
+    ],
+    [
+      0.231403,
+      "d"
+    ],
+    [
+      0.299061,
+      "!"
+    ],
+    [
+      2.000000,
+      "\r\n"
+    ],
+    [
+      0.000307,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.304768,
+      "#"
+    ],
+    [
+      0.229433,
+      " "
+    ],
+    [
+      0.647220,
+      "t"
+    ],
+    [
+      0.070692,
+      "h"
+    ],
+    [
+      0.349432,
+      "a"
+    ],
+    [
+      0.112924,
+      "n"
+    ],
+    [
+      0.207031,
+      "k"
+    ],
+    [
+      0.567641,
+      "s"
+    ],
+    [
+      0.121708,
+      " "
+    ],
+    [
+      0.135723,
+      "f"
+    ],
+    [
+      0.139102,
+      "o"
+    ],
+    [
+      0.060453,
+      "r"
+    ],
+    [
+      0.152408,
+      " "
+    ],
+    [
+      0.116234,
+      "v"
+    ],
+    [
+      0.142885,
+      "i"
+    ],
+    [
+      0.106596,
+      "e"
+    ],
+    [
+      0.231115,
+      "w"
+    ],
+    [
+      0.416046,
+      "i"
+    ],
+    [
+      0.086563,
+      "n"
+    ],
+    [
+      0.144009,
+      "g"
+    ],
+    [
+      0.725139,
+      "!"
+    ],
+    [
+      0.299810,
+      "\r\n"
+    ],
+    [
+      0.000250,
+      "\u001b]0;tw@tux: ~/borg/demo\u0007tw@tux:~/borg/demo$ "
+    ],
+    [
+      0.710767,
+      "exit"
+    ],
+    [
+      0.000006,
+      "\r\n"
+    ]
+  ]
+}

+ 51 - 0
docs/misc/asciinema/install_and_basics.txt

@@ -0,0 +1,51 @@
+# borgbackup - installation and basic usage
+
+# I have already downloaded the binary release from github:
+ls -l
+# binary file + GPG signature
+
+# verifying whether the binary is valid:
+gpg --verify borg-linux64.asc borg-linux64
+
+# install it as "borg":
+cp borg-linux64 ~/bin/borg
+
+# making it executable:
+chmod +x ~/bin/borg
+
+# yay, installation done! let's make backups!
+
+# creating a repository:
+borg init repo
+
+# creating our first backup with stuff from "data" directory:
+borg create --stats --progress --compression lz4 repo::backup1 data
+
+# changing the data slightly:
+echo "some more data" > data/one_file_more
+
+# creating another backup:
+borg create --stats --progress repo::backup2 data
+
+# that was much faster! it recognized/deduplicated unchanged files.
+# see the "Deduplicated size" column for "This archive"! :)
+
+# extracting a backup archive:
+mv data data.orig
+borg extract repo::backup2
+
+# checking if restored data differs from original data:
+diff -r data.orig data
+
+# no, it doesn't! :)
+
+# listing the repo contents:
+borg list repo
+
+# listing the backup2 archive contents (shortened):
+borg list repo::backup2 | tail
+
+# easy, isn't it?
+
+# if you like #borgbackup, spread the word!
+

+ 8 - 4
docs/quickstart.rst

@@ -100,17 +100,17 @@ Backup compression
 Default is no compression, but we support different methods with high speed
 Default is no compression, but we support different methods with high speed
 or high compression:
 or high compression:
 
 
-If you have a quick repo storage and you want a little compression:
+If you have a quick repo storage and you want a little compression: ::
 
 
     $ borg create --compression lz4 /mnt/backup::repo ~
     $ borg create --compression lz4 /mnt/backup::repo ~
 
 
 If you have a medium fast repo storage and you want a bit more compression (N=0..9,
 If you have a medium fast repo storage and you want a bit more compression (N=0..9,
-0 means no compression, 9 means high compression):
+0 means no compression, 9 means high compression): ::
 
 
     $ borg create --compression zlib,N /mnt/backup::repo ~
     $ borg create --compression zlib,N /mnt/backup::repo ~
 
 
 If you have a very slow repo storage and you want high compression (N=0..9, 0 means
 If you have a very slow repo storage and you want high compression (N=0..9, 0 means
-low compression, 9 means high compression):
+low compression, 9 means high compression): ::
 
 
     $ borg create --compression lzma,N /mnt/backup::repo ~
     $ borg create --compression lzma,N /mnt/backup::repo ~
 
 
@@ -150,7 +150,11 @@ by providing the correct passphrase.
 For automated backups the passphrase can be specified using the
 For automated backups the passphrase can be specified using the
 `BORG_PASSPHRASE` environment variable.
 `BORG_PASSPHRASE` environment variable.
 
 
-**The repository data is totally inaccessible without the key:**
+.. note:: Be careful about how you set that environment, see
+          :ref:`this note about password environments <password_env>`
+          for more information.
+
+.. important:: The repository data is totally inaccessible without the key:**
     Make a backup copy of the key file (``keyfile`` mode) or repo config
     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
     file (``repokey`` mode) and keep it at a safe place, so you still have
     the key in case it gets corrupted or lost.
     the key in case it gets corrupted or lost.

+ 66 - 39
docs/usage.rst

@@ -22,19 +22,15 @@ Return codes
 
 
 ::
 ::
 
 
-    0      no error, normal termination
-    1      some error occurred (this can be a complete or a partial failure)
-    128+N  killed by signal N (e.g. 137 == kill -9)
+    0 = success (logged as INFO)
+    1 = warning (operation reached its normal end, but there were warnings -
+        you should check the log, logged as WARNING)
+    2 = error (like a fatal error, a local or remote exception, the operation
+        did not reach its normal end, logged as ERROR)
+    128+N = killed by signal N (e.g. 137 == kill -9)
 
 
+The return code is also logged at the indicated level as the last log entry.
 
 
-Note: we are aware that more distinct return codes might be useful, but it is
-not clear yet which return codes should be used for which precise conditions.
-
-See issue #61 for a discussion about that. Depending on the outcome of the
-discussion there, return codes may change in future (the only thing rather sure
-is that 0 will always mean some sort of success and "not 0" will always mean
-some sort of warning / error / failure - but the definition of success might
-change).
 
 
 Environment Variables
 Environment Variables
 ---------------------
 ---------------------
@@ -60,6 +56,12 @@ Some "yes" sayers (if set, they automatically confirm that you really want to do
         For "Warning: The repository at location ... was previously located at ..."
         For "Warning: The repository at location ... was previously located at ..."
     BORG_CHECK_I_KNOW_WHAT_I_AM_DOING
     BORG_CHECK_I_KNOW_WHAT_I_AM_DOING
         For "Warning: 'check --repair' is an experimental feature that might result in data loss."
         For "Warning: 'check --repair' is an experimental feature that might result in data loss."
+    BORG_CYTHON_DISABLE
+        Disables the loading of Cython modules. This is currently
+        experimental and is used only to generate usage docs at build
+        time. It is unlikely to produce good results on a regular
+        run. The variable should be set to the name of the  calling class, and
+        should be unique across all of borg. It is currently only used by ``build_usage``.
 
 
 Directories:
 Directories:
     BORG_KEYS_DIR
     BORG_KEYS_DIR
@@ -128,6 +130,19 @@ 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.
 
 
 
 
+Units
+-----
+
+To display quantities, |project_name| takes care of respecting the
+usual conventions of scale. Disk sizes are displayed in `decimal
+<https://en.wikipedia.org/wiki/Decimal>`_, using powers of ten (so
+``kB`` means 1000 bytes). For memory usage, `binary prefixes
+<https://en.wikipedia.org/wiki/Binary_prefix>`_ are used, and are
+indicated using the `IEC binary prefixes
+<https://en.wikipedia.org/wiki/IEC_80000-13#Prefixes_for_binary_multiples>`_,
+using powers of two (so ``KiB`` means 1024 bytes).
+
+
 .. include:: usage/init.rst.inc
 .. include:: usage/init.rst.inc
 
 
 Examples
 Examples
@@ -195,8 +210,9 @@ Examples
         --exclude '*.pyc'
         --exclude '*.pyc'
 
 
     # Backup the root filesystem into an archive named "root-YYYY-MM-DD"
     # Backup the root filesystem into an archive named "root-YYYY-MM-DD"
+    # use zlib compression (good, but slow) - default is no compression
     NAME="root-`date +%Y-%m-%d`"
     NAME="root-`date +%Y-%m-%d`"
-    $ borg create /mnt/backup::$NAME / --do-not-cross-mountpoints
+    $ borg create -C zlib,6 /mnt/backup::$NAME / --do-not-cross-mountpoints
 
 
     # Backup huge files with little chunk management overhead
     # Backup huge files with little chunk management overhead
     $ borg create --chunker-params 19,23,21,4095 /mnt/backup::VMs /srv/VMs
     $ borg create --chunker-params 19,23,21,4095 /mnt/backup::VMs /srv/VMs
@@ -239,6 +255,8 @@ Note: currently, extract always writes into the current working directory ("."),
 
 
 .. include:: usage/check.rst.inc
 .. include:: usage/check.rst.inc
 
 
+.. include:: usage/rename.rst.inc
+
 .. include:: usage/delete.rst.inc
 .. include:: usage/delete.rst.inc
 
 
 .. include:: usage/list.rst.inc
 .. include:: usage/list.rst.inc
@@ -309,7 +327,7 @@ Examples
     Hostname: myhostname
     Hostname: myhostname
     Username: root
     Username: root
     Time: Fri Aug  2 15:18:17 2013
     Time: Fri Aug  2 15:18:17 2013
-    Command line: /usr/bin/borg create --stats /mnt/backup::root-2013-08-02 / --do-not-cross-mountpoints
+    Command line: /usr/bin/borg create --stats -C zlib,6 /mnt/backup::root-2013-08-02 / --do-not-cross-mountpoints
     Number of files: 147429
     Number of files: 147429
     Original size: 5344169493 (4.98 GB)
     Original size: 5344169493 (4.98 GB)
     Compressed size: 1748189642 (1.63 GB)
     Compressed size: 1748189642 (1.63 GB)
@@ -362,65 +380,74 @@ Examples
     command="borg serve --restrict-to-path /mnt/backup" ssh-rsa AAAAB3[...]
     command="borg serve --restrict-to-path /mnt/backup" ssh-rsa AAAAB3[...]
 
 
 
 
+Miscellaneous Help
+------------------
+
+.. include:: usage/help.rst.inc
+
+
 Additional Notes
 Additional Notes
-================
+----------------
 
 
 Here are misc. notes about topics that are maybe not covered in enough detail in the usage section.
 Here are misc. notes about topics that are maybe not covered in enough detail in the usage section.
 
 
 --read-special
 --read-special
---------------
+~~~~~~~~~~~~~~
 
 
-The option --read-special is not intended for normal, filesystem-level (full or
+The option ``--read-special`` is not intended for normal, filesystem-level (full or
 partly-recursive) backups. You only give this option if you want to do something
 partly-recursive) backups. You only give this option if you want to do something
-rather ... special - and if you have hand-picked some files that you want to treat
+rather ... special -- and if you have hand-picked some files that you want to treat
 that way.
 that way.
 
 
-`borg create --read-special` will open all files without doing any special treatment
-according to the file type (the only exception here are directories: they will be
-recursed into). Just imagine what happens if you do `cat filename` - the content
-you will see there is what borg will backup for that filename.
+``borg create --read-special`` will open all files without doing any special
+treatment according to the file type (the only exception here are directories:
+they will be recursed into). Just imagine what happens if you do ``cat
+filename`` --- the content you will see there is what borg will backup for that
+filename.
 
 
 So, for example, symlinks will be followed, block device content will be read,
 So, for example, symlinks will be followed, block device content will be read,
 named pipes / UNIX domain sockets will be read.
 named pipes / UNIX domain sockets will be read.
 
 
-You need to be careful with what you give as filename when using --read-special,
-e.g. if you give /dev/zero, your backup will never terminate.
+You need to be careful with what you give as filename when using ``--read-special``,
+e.g. if you give ``/dev/zero``, your backup will never terminate.
 
 
-The given files' metadata is saved as it would be saved without --read-special
-(e.g. its name, its size [might be 0], its mode, etc.) - but additionally, also
-the content read from it will be saved for it.
+The given files' metadata is saved as it would be saved without
+``--read-special`` (e.g. its name, its size [might be 0], its mode, etc.) - but
+additionally, also the content read from it will be saved for it.
 
 
-Restoring such files' content is currently only supported one at a time via --stdout
-option (and you have to redirect stdout to where ever it shall go, maybe directly
-into an existing device file of your choice or indirectly via dd).
+Restoring such files' content is currently only supported one at a time via
+``--stdout`` option (and you have to redirect stdout to where ever it shall go,
+maybe directly into an existing device file of your choice or indirectly via
+``dd``).
 
 
 Example
 Example
 ~~~~~~~
 ~~~~~~~
 
 
 Imagine you have made some snapshots of logical volumes (LVs) you want to backup.
 Imagine you have made some snapshots of logical volumes (LVs) you want to backup.
 
 
-Note: For some scenarios, this is a good method to get "crash-like" consistency
-(I call it crash-like because it is the same as you would get if you just hit the
-reset button or your machine would abrubtly and completely crash).
-This is better than no consistency at all and a good method for some use cases,
-but likely not good enough if you have databases running.
+.. note::
+
+    For some scenarios, this is a good method to get "crash-like" consistency
+    (I call it crash-like because it is the same as you would get if you just
+    hit the reset button or your machine would abrubtly and completely crash).
+    This is better than no consistency at all and a good method for some use
+    cases, but likely not good enough if you have databases running.
 
 
 Then you create a backup archive of all these snapshots. The backup process will
 Then you create a backup archive of all these snapshots. The backup process will
 see a "frozen" state of the logical volumes, while the processes working in the
 see a "frozen" state of the logical volumes, while the processes working in the
 original volumes continue changing the data stored there.
 original volumes continue changing the data stored there.
 
 
-You also add the output of `lvdisplay` to your backup, so you can see the LV sizes
-in case you ever need to recreate and restore them.
+You also add the output of ``lvdisplay`` to your backup, so you can see the LV
+sizes in case you ever need to recreate and restore them.
 
 
-After the backup has completed, you remove the snapshots again.
+After the backup has completed, you remove the snapshots again. ::
 
 
-::
     $ # create snapshots here
     $ # create snapshots here
     $ lvdisplay > lvdisplay.txt
     $ lvdisplay > lvdisplay.txt
     $ borg create --read-special /mnt/backup::repo lvdisplay.txt /dev/vg0/*-snapshot
     $ borg create --read-special /mnt/backup::repo lvdisplay.txt /dev/vg0/*-snapshot
     $ # remove snapshots here
     $ # remove snapshots here
 
 
-Now, let's see how to restore some LVs from such a backup.
+Now, let's see how to restore some LVs from such a backup. ::
 
 
     $ borg extract /mnt/backup::repo lvdisplay.txt
     $ borg extract /mnt/backup::repo lvdisplay.txt
     $ # create empty LVs with correct sizes here (look into lvdisplay.txt).
     $ # create empty LVs with correct sizes here (look into lvdisplay.txt).

+ 1 - 0
requirements.d/development.txt

@@ -2,4 +2,5 @@ tox
 mock
 mock
 pytest
 pytest
 pytest-cov<2.0.0
 pytest-cov<2.0.0
+pytest-benchmark==3.0.0b1
 Cython
 Cython

+ 2 - 4
setup.cfg

@@ -2,7 +2,5 @@
 python_files = testsuite/*.py
 python_files = testsuite/*.py
 
 
 [flake8]
 [flake8]
-ignore = E226,F403
-max-line-length = 250
-exclude = docs/conf.py,borg/_version.py,build,dist,.git,.idea,.cache
-max-complexity = 100
+max-line-length = 120
+exclude = build,dist,.git,.idea,.cache,.tox

+ 142 - 14
setup.py

@@ -1,8 +1,15 @@
 # -*- encoding: utf-8 *-*
 # -*- encoding: utf-8 *-*
 import os
 import os
+import re
 import sys
 import sys
 from glob import glob
 from glob import glob
 
 
+from distutils.command.build import build
+from distutils.core import Command
+from distutils.errors import DistutilsOptionError
+from distutils import log
+from setuptools.command.build_py import build_py
+
 min_python = (3, 2)
 min_python = (3, 2)
 my_python = sys.version_info
 my_python = sys.version_info
 
 
@@ -10,6 +17,9 @@ if my_python < min_python:
     print("Borg requires Python %d.%d or later" % min_python)
     print("Borg requires Python %d.%d or later" % min_python)
     sys.exit(1)
     sys.exit(1)
 
 
+# Are we building on ReadTheDocs?
+on_rtd = os.environ.get('READTHEDOCS')
+
 # msgpack pure python data corruption was fixed in 0.4.6.
 # msgpack pure python data corruption was fixed in 0.4.6.
 # Also, we might use some rather recent API features.
 # Also, we might use some rather recent API features.
 install_requires=['msgpack-python>=0.4.6', ]
 install_requires=['msgpack-python>=0.4.6', ]
@@ -62,7 +72,7 @@ except ImportError:
     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 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_linux_source, platform_freebsd_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.')
@@ -101,31 +111,149 @@ 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']
 if os.environ.get('BORG_LZ4_PREFIX'):
 if os.environ.get('BORG_LZ4_PREFIX'):
-    possible_openssl_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)
-if not lz4_prefix:
+if lz4_prefix:
+    include_dirs.append(os.path.join(lz4_prefix, 'include'))
+    library_dirs.append(os.path.join(lz4_prefix, 'lib'))
+elif not on_rtd:
     raise Exception('Unable to find LZ4 headers. (Looked here: {})'.format(', '.join(possible_lz4_prefixes)))
     raise Exception('Unable to find LZ4 headers. (Looked here: {})'.format(', '.join(possible_lz4_prefixes)))
-include_dirs.append(os.path.join(lz4_prefix, 'include'))
-library_dirs.append(os.path.join(lz4_prefix, 'lib'))
 
 
 
 
 with open('README.rst', 'r') as fd:
 with open('README.rst', 'r') as fd:
     long_description = fd.read()
     long_description = fd.read()
 
 
-cmdclass = {'build_ext': build_ext, 'sdist': Sdist}
+class build_usage(Command):
+    description = "generate usage for each command"
+
+    user_options = [
+        ('output=', 'O', 'output directory'),
+    ]
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        print('generating usage docs')
+        # allows us to build docs without the C modules fully loaded during help generation
+        if 'BORG_CYTHON_DISABLE' not in os.environ:
+            os.environ['BORG_CYTHON_DISABLE'] = self.__class__.__name__
+        from borg.archiver import Archiver
+        parser = Archiver().build_parser(prog='borg')
+        choices = {}
+        for action in parser._actions:
+            if action.choices is not None:
+                choices.update(action.choices)
+        print('found commands: %s' % list(choices.keys()))
+        if not os.path.exists('docs/usage'):
+            os.mkdir('docs/usage')
+        for command, parser in choices.items():
+            print('generating help for %s' % command)
+            with open('docs/usage/%s.rst.inc' % command, 'w') as doc:
+                if command == 'help':
+                    for topic in Archiver.helptext:
+                        params = {"topic": topic,
+                                  "underline": '~' * len('borg help ' + topic)}
+                        doc.write(".. _borg_{topic}:\n\n".format(**params))
+                        doc.write("borg help {topic}\n{underline}\n::\n\n".format(**params))
+                        doc.write(Archiver.helptext[topic])
+                else:
+                    params = {"command": command,
+                              "underline": '-' * len('borg ' + command)}
+                    doc.write(".. _borg_{command}:\n\n".format(**params))
+                    doc.write("borg {command}\n{underline}\n::\n\n".format(**params))
+                    epilog = parser.epilog
+                    parser.epilog = None
+                    doc.write(re.sub("^", "    ", parser.format_help(), flags=re.M))
+                    doc.write("\nDescription\n~~~~~~~~~~~\n")
+                    doc.write(epilog)
+        # return to regular Cython configuration, if we changed it
+        if os.environ.get('BORG_CYTHON_DISABLE') == self.__class__.__name__:
+            del os.environ['BORG_CYTHON_DISABLE']
+
+
+class build_api(Command):
+    description = "generate a basic api.rst file based on the modules available"
+
+    user_options = [
+        ('output=', 'O', 'output directory'),
+    ]
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        print("auto-generating API documentation")
+        with open("docs/api.rst", "w") as doc:
+            doc.write("""
+API Documentation
+=================
+""")
+            for mod in glob('borg/*.py') + glob('borg/*.pyx'):
+                print("examining module %s" % mod)
+                mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.')
+                if "._" not in mod:
+                    doc.write("""
+.. automodule:: %s
+    :members:
+    :undoc-members:
+""" % mod)
+
+# (function, predicate), see http://docs.python.org/2/distutils/apiref.html#distutils.cmd.Command.sub_commands
+# seems like this doesn't work on RTD, see below for build_py hack.
+build.sub_commands.append(('build_api', None))
+build.sub_commands.append(('build_usage', None))
+
+
+class build_py_custom(build_py):
+    """override build_py to also build our stuff
+
+    it is unclear why this is necessary, but in some environments
+    (Readthedocs.org, specifically), the above
+    ``build.sub_commands.append()`` doesn't seem to have an effect:
+    our custom build commands seem to be ignored when running
+    ``setup.py install``.
+
+    This class overrides the ``build_py`` target by forcing it to run
+    our custom steps as well.
+
+    See also the `bug report on RTD
+    <https://github.com/rtfd/readthedocs.org/issues/1740>`_.
+    """
+    def run(self):
+        super().run()
+        self.announce('calling custom build steps', level=log.INFO)
+        self.run_command('build_ext')
+        self.run_command('build_api')
+        self.run_command('build_usage')
+
+
+cmdclass = {
+    'build_ext': build_ext,
+    'build_api': build_api,
+    'build_usage': build_usage,
+    'build_py': build_py_custom,
+    'sdist': Sdist
+}
 
 
-ext_modules = [
+ext_modules = []
+if not on_rtd:
+    ext_modules += [
     Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs),
     Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs),
     Extension('borg.crypto', [crypto_source], libraries=['crypto'], include_dirs=include_dirs, library_dirs=library_dirs),
     Extension('borg.crypto', [crypto_source], libraries=['crypto'], include_dirs=include_dirs, library_dirs=library_dirs),
     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'):
-    ext_modules.append(Extension('borg.platform_linux', [platform_linux_source], libraries=['acl']))
-elif sys.platform.startswith('freebsd'):
-    ext_modules.append(Extension('borg.platform_freebsd', [platform_freebsd_source]))
-elif sys.platform == 'darwin':
-    ext_modules.append(Extension('borg.platform_darwin', [platform_darwin_source]))
+    if sys.platform.startswith('linux'):
+        ext_modules.append(Extension('borg.platform_linux', [platform_linux_source], libraries=['acl']))
+    elif sys.platform.startswith('freebsd'):
+        ext_modules.append(Extension('borg.platform_freebsd', [platform_freebsd_source]))
+    elif sys.platform == 'darwin':
+        ext_modules.append(Extension('borg.platform_darwin', [platform_darwin_source]))
 
 
 setup(
 setup(
     name='borgbackup',
     name='borgbackup',
@@ -134,7 +262,7 @@ setup(
     },
     },
     author='The Borg Collective (see AUTHORS file)',
     author='The Borg Collective (see AUTHORS file)',
     author_email='borgbackup@librelist.com',
     author_email='borgbackup@librelist.com',
-    url='https://borgbackup.github.io/',
+    url='https://borgbackup.readthedocs.org/',
     description='Deduplicated, encrypted, authenticated and compressed backups',
     description='Deduplicated, encrypted, authenticated and compressed backups',
     long_description=long_description,
     long_description=long_description,
     license='BSD',
     license='BSD',

+ 1 - 1
tox.ini

@@ -11,6 +11,6 @@ changedir = {toxworkdir}
 deps =
 deps =
      -rrequirements.d/development.txt
      -rrequirements.d/development.txt
      attic
      attic
-commands = py.test --cov=borg --pyargs {posargs:borg.testsuite}
+commands = py.test --cov=borg --benchmark-skip --pyargs {posargs:borg.testsuite}
 # fakeroot -u needs some env vars:
 # fakeroot -u needs some env vars:
 passenv = *
 passenv = *