Bladeren bron

merged master

Thomas Waldmann 9 jaren geleden
bovenliggende
commit
8af3aa3397

+ 17 - 0
.coveragerc

@@ -0,0 +1,17 @@
+[run]
+branch = True
+source = borg
+omit =
+    borg/__init__.py
+    borg/__main__.py
+    borg/_version.py
+
+[report]
+exclude_lines =
+    pragma: no cover
+    def __repr__
+    raise AssertionError
+    raise NotImplementedError
+    if 0:
+    if __name__ == .__main__.:
+ignore_errors = True

+ 1 - 0
.gitignore

@@ -21,3 +21,4 @@ docs/usage/*.inc
 borg.build/
 borg.build/
 borg.dist/
 borg.dist/
 borg.exe
 borg.exe
+.coverage

+ 45 - 13
.travis.yml

@@ -1,15 +1,47 @@
+sudo: required
+
 language: python
 language: python
-python:
-  - "3.2"
-  - "3.3"
-  - "3.4"
-# command to install dependencies
+
+cache:
+    directories:
+        - $HOME/.cache/pip
+
+matrix:
+    include:
+        - python: 3.2
+          os: linux
+          env: TOXENV=py32
+        - python: 3.3
+          os: linux
+          env: TOXENV=py33
+        - python: 3.4
+          os: linux
+          env: TOXENV=py34
+        - language: generic
+          os: osx
+          osx_image: xcode6.4
+          env: TOXENV=py32
+        - language: generic
+          os: osx
+          osx_image: xcode6.4
+          env: TOXENV=py33
+        - language: generic
+          os: osx
+          osx_image: xcode6.4
+          env: TOXENV=py34
+
 install:
 install:
-  - "sudo add-apt-repository -y ppa:gezakovacs/lz4"
-  - "sudo apt-get update"
-  - "sudo apt-get install -y liblz4-dev"
-  - "sudo apt-get install -y libacl1-dev"
-  - "pip install --use-mirrors Cython"
-  - "pip install -e ."
-# command to run tests
-script: fakeroot -u py.test
+    - ./.travis/install.sh
+
+script:
+    - ./.travis/run.sh
+
+after_success:
+    - ./.travis/upload_coverage.sh
+
+notifications:
+    irc:
+        channels:
+            - "irc.freenode.org#borgbackup"
+        use_notice: true
+        skip_join: true

+ 46 - 0
.travis/install.sh

@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -e
+set -x
+
+if [[ "$(uname -s)" == 'Darwin' ]]; then
+    brew update || brew update
+
+    if [[ "${OPENSSL}" != "0.9.8" ]]; then
+        brew outdated openssl || brew upgrade openssl
+    fi
+
+    if which pyenv > /dev/null; then
+        eval "$(pyenv init -)"
+    fi
+
+    brew outdated pyenv || brew upgrade pyenv
+
+    case "${TOXENV}" in
+        py32)
+            pyenv install 3.2.6
+            pyenv global 3.2.6
+            ;;
+        py33)
+            pyenv install 3.3.6
+            pyenv global 3.3.6
+            ;;
+        py34)
+            pyenv install 3.4.3
+            pyenv global 3.4.3
+            ;;
+    esac
+    pyenv rehash
+    python -m pip install --user virtualenv
+else
+    pip install virtualenv
+    sudo add-apt-repository -y ppa:gezakovacs/lz4
+    sudo apt-get update
+    sudo apt-get install -y liblz4-dev
+    sudo apt-get install -y libacl1-dev
+fi
+
+python -m virtualenv ~/.venv
+source ~/.venv/bin/activate
+pip install tox pytest pytest-cov codecov Cython
+pip install -e .

+ 23 - 0
.travis/run.sh

@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -e
+set -x
+
+if [[ "$(uname -s)" == "Darwin" ]]; then
+    eval "$(pyenv init -)"
+    if [[ "${OPENSSL}" != "0.9.8" ]]; then
+        # set our flags to use homebrew openssl
+        export ARCHFLAGS="-arch x86_64"
+        export LDFLAGS="-L/usr/local/opt/openssl/lib"
+        export CFLAGS="-I/usr/local/opt/openssl/include"
+    fi
+fi
+
+source ~/.venv/bin/activate
+
+if [[ "$(uname -s)" == "Darwin" ]]; then
+    # no fakeroot on OS X
+    sudo tox -e $TOXENV
+else
+    fakeroot -u tox
+fi

+ 13 - 0
.travis/upload_coverage.sh

@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -e
+set -x
+
+NO_COVERAGE_TOXENVS=(pep8)
+if ! [[ "${NO_COVERAGE_TOXENVS[*]}" =~ "${TOXENV}" ]]; then
+    source ~/.venv/bin/activate
+    ln .tox/.coverage .coverage
+    # on osx, tests run as root, need access to .coverage
+    sudo chmod 666 .coverage
+    codecov -e TRAVIS_OS_NAME TOXENV
+fi

+ 41 - 3
CHANGES → CHANGES.rst

@@ -5,6 +5,22 @@ Borg Changelog
 Version 0.24.0
 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:
 New features:
 
 
 - borg create --chunker-params ... to configure the chunker, fixes #16
 - borg create --chunker-params ... to configure the chunker, fixes #16
@@ -17,12 +33,21 @@ New features:
 - borg create --compression 0..9 to select zlib compression level, fixes #66
 - borg create --compression 0..9 to select zlib compression level, fixes #66
   (attic #295).
   (attic #295).
 - borg init --encryption repokey (to store the encryption key into the repo),
 - borg init --encryption repokey (to store the encryption key into the repo),
-  deprecate --encryption passphrase, fixes #85
+  fixes #85
 - improve at-end error logging, always log exceptions and set exit_code=1
 - improve at-end error logging, always log exceptions and set exit_code=1
 - LoggedIO: better error checks / exceptions / exception handling
 - 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:
 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
 - more compatible repository locking code (based on mkdir), maybe fixes #92
   (attic #317, attic #201).
   (attic #317, attic #201).
 - better Exception msg if no Borg is installed on the remote repo server, #56
 - better Exception msg if no Borg is installed on the remote repo server, #56
@@ -30,10 +55,12 @@ Bug fixes:
   fixes attic #326.
   fixes attic #326.
 - fix Traceback when running check --repair, attic #232
 - fix Traceback when running check --repair, attic #232
 - clarify help text, fixes #73.
 - clarify help text, fixes #73.
+- add help string for --no-files-cache, fixes #140
 
 
 Other changes:
 Other changes:
 
 
 - improved docs:
 - improved docs:
+
   - added docs/misc directory for misc. writeups that won't be included
   - added docs/misc directory for misc. writeups that won't be included
     "as is" into the html docs.
     "as is" into the html docs.
   - document environment variables and return codes (attic #324, attic #52)
   - document environment variables and return codes (attic #324, attic #52)
@@ -44,14 +71,25 @@ Other changes:
   - add FAQ entries about redundancy / integrity
   - add FAQ entries about redundancy / integrity
   - clarify that borg extract uses the cwd as extraction target
   - clarify that borg extract uses the cwd as extraction target
   - update internals doc about chunker params, memory usage and compression
   - 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
 - use borg-tmp as prefix for temporary files / directories
 - short prune options without "keep-" are deprecated, do not suggest them
 - short prune options without "keep-" are deprecated, do not suggest them
-- improved tox configuration, documented there how to invoke it
+- improved tox configuration
 - remove usage of unittest.mock, always use mock from pypi
 - remove usage of unittest.mock, always use mock from pypi
 - use entrypoints instead of scripts, for better use of the wheel format and
 - use entrypoints instead of scripts, for better use of the wheel format and
   modern installs
   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:
 I forgot to list some stuff already implemented in 0.23.0, here they are:
 
 
 New features:
 New features:

+ 1 - 1
MANIFEST.in

@@ -1,4 +1,4 @@
-include README.rst AUTHORS LICENSE CHANGES MANIFEST.in versioneer.py
+include README.rst AUTHORS LICENSE CHANGES.rst MANIFEST.in versioneer.py
 recursive-include borg *.pyx
 recursive-include borg *.pyx
 recursive-include docs *
 recursive-include docs *
 recursive-exclude docs *.pyc
 recursive-exclude docs *.pyc

+ 109 - 76
README.rst

@@ -1,96 +1,129 @@
-|build|
+What is BorgBackup?
+-------------------
+BorgBackup (short: Borg) is a deduplicating backup program.
+Optionally, it supports compression and authenticated encryption.
 
 
-What is Borg?
--------------
-Borg is a deduplicating backup program. The main goal of Borg is to provide
-an efficient and secure way to backup data. The data deduplication
-technique used makes Borg suitable for daily backups since only changes
-are stored.
+The main goal of Borg is to provide an efficient and secure way to backup data.
+The data deduplication technique used makes Borg suitable for daily backups
+since only changes are stored.
+The authenticated encryption technique makes it suitable for backups to not
+fully trusted targets.
 
 
-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 Installation docs <http://borgbackup.github.io/borgbackup/installation.html>`_
 
 
-BORG IS NOT COMPATIBLE WITH ORIGINAL ATTIC.
-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.
 
 
-NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES.
+Main features
+~~~~~~~~~~~~~
+**Space efficient storage**
+  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
+  and only chunks that have never been seen before are added to the repository.
 
 
-THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS.
+  To deduplicate, all the chunks in the same repository are considered, no
+  matter whether they come from different machines, from previous backups,
+  from the same backup or even from the same single file.
+
+  Compared to other deduplication approaches, this method does NOT depend on:
+
+  * file/directory names staying the same
+
+    So you can move your stuff around without killing the deduplication,
+    even between machines sharing a repo.
+
+  * complete files or time stamps staying the same
+
+    If a big file changes a little, only a few new chunks will be stored -
+    this is great for VMs or raw disks.
+
+  * the absolute position of a data chunk inside a file
+
+    Stuff may get shifted and will still be found by the deduplication
+    algorithm.
+
+**Speed**
+  * performance critical code (chunking, compression, encryption) is
+    implemented in C/Cython
+  * local caching of files/chunks index data
+  * quick detection of unmodified files
+
+**Data encryption**
+    All data can be protected using 256-bit AES encryption, data integrity and
+    authenticity is verified using HMAC-SHA256.
+
+**Compression**
+    All data can be compressed by zlib, level 0-9.
+
+**Off-site backups**
+    Borg can store data on any remote host accessible over SSH.  If Borg is
+    installed on the remote host, big performance gains can be achieved
+    compared to using a network filesystem (sshfs, nfs, ...).
+
+**Backups mountable as filesystems**
+    Backup archives are mountable as userspace filesystems for easy interactive
+    backup examination and restores (e.g. by using a regular file manager).
 
 
-Read `issue #1 <https://github.com/borgbackup/borg/issues/1>`_ on the issue tracker, goals are being defined there.
+**Platforms Borg works on**
+  * Linux
+  * FreeBSD
+  * Mac OS X
+  * Cygwin (unsupported)
+
+**Free and Open Source Software**
+  * security and functionality can be audited independently
+  * licensed under the BSD (3-clause) license
 
 
-Please also see the `LICENSE  <https://github.com/borgbackup/borg/blob/master/LICENSE>`_ for more informations.
 
 
 Easy to use
 Easy to use
 ~~~~~~~~~~~
 ~~~~~~~~~~~
-Initialize 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
-    $ borg create -v /mnt/backup::documents ~/Documents
+    $ borg create /mnt/backup::Monday ~/Documents
 
 
-For a graphical frontend refer to our complementary project `BorgWeb <https://github.com/borgbackup/borgweb>`_.
+Now doing another backup, just to show off the great deduplication::
 
 
-Main features
-~~~~~~~~~~~~~
-Space efficient storage
-  Variable block size deduplication is used to reduce the number of bytes 
-  stored by detecting redundant data. Each file is split into a number of
-  variable length chunks and only chunks that have never been seen before are
-  compressed and added to the repository.
-
-  The content-defined chunking based deduplication is applied to remove
-  duplicate chunks within: 
-
-  * the current backup data set (even inside single files / streams)
-  * current and previous backups of same machine
-  * all the chunks in the same repository, even if coming from other machines
-
-  This advanced deduplication method does NOT depend on:
- 
-  * file/directory names staying the same (so you can move your stuff around
-    without killing the deduplication, even between machines sharing a repo)
-  * complete files or time stamps staying the same (if a big file changes a
-    little, only a few new chunks will be stored - this is great for VMs or
-    raw disks)
-  * the absolute position of a data chunk inside a file (stuff may get shifted
-    and will still be found by the deduplication algorithm)
-
-Optional data encryption
-    All data can be protected using 256-bit AES encryption and data integrity
-    and authenticity is verified using HMAC-SHA256.
-
-Off-site backups
-    Borg can store data on any remote host accessible over SSH.  This is
-    most efficient if Borg is also installed on the remote host.
-
-Backups mountable as filesystems
-    Backup archives are mountable as userspace filesystems for easy backup
-    verification and restores.
-
-What do I need?
----------------
-Borg requires Python 3.2 or above to work.
-Borg also requires a sufficiently recent OpenSSL (>= 1.0.0).
-In order to mount archives as filesystems, llfuse is required.
-
-How do I install it?
---------------------
-::
-
-  $ pip3 install borgbackup
-
-Where are the docs?
--------------------
-Go to https://borgbackup.github.io/ for a prebuilt version of the documentation.
-You can also build it yourself from the docs folder.
+    $ borg create --stats /mnt/backup::Tuesday ~/Documents
+
+    Archive name: Tuesday
+    Archive fingerprint: 387a5e3f9b0e792e91c...
+    Start time: Tue Mar 25 12:00:10 2014
+    End time:   Tue Mar 25 12:00:10 2014
+    Duration: 0.08 seconds
+    Number of files: 358
+                      Original size    Compressed size    Deduplicated size
+    This archive:          57.16 MB           46.78 MB            151.67 kB  <--- !
+    All archives:         114.02 MB           93.46 MB             44.81 MB
 
 
-Where are the tests?
---------------------
-The tests are in the borg/testsuite package. To run the test suite use the
-following command::
+For a graphical frontend refer to our complementary project
+`BorgWeb <https://github.com/borgbackup/borgweb>`_.
 
 
-  $ fakeroot -u tox  # you need to have tox and pytest installed
+
+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>`_".
+
+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.
+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.
+
+NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES.
+
+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>`_.
+
+|build| |coverage|
 
 
 .. |build| image:: https://travis-ci.org/borgbackup/borg.svg
 .. |build| image:: https://travis-ci.org/borgbackup/borg.svg
         :alt: Build Status
         :alt: Build Status
         :target: https://travis-ci.org/borgbackup/borg
         :target: https://travis-ci.org/borgbackup/borg
+
+.. |coverage| image:: http://codecov.io/github/borgbackup/borg/coverage.svg?branch=master
+        :alt: Test Coverage
+        :target: http://codecov.io/github/borgbackup/borg?branch=master

+ 19 - 0
borg/_hashindex.c

@@ -385,3 +385,22 @@ hashindex_summarize(HashIndex *index, long long *total_size, long long *total_cs
     *total_unique_chunks = unique_chunks;
     *total_unique_chunks = unique_chunks;
     *total_chunks = chunks;
     *total_chunks = chunks;
 }
 }
+
+static void
+hashindex_merge(HashIndex *index, HashIndex *other)
+{
+    int32_t key_size = index->key_size;
+    const int32_t *other_values;
+    int32_t *my_values;
+    void *key = NULL;
+
+    while((key = hashindex_next_key(other, key))) {
+        other_values = key + key_size;
+        my_values = (int32_t *)hashindex_get(index, key);
+        if(my_values == NULL) {
+            hashindex_set(index, key, other_values);
+        } else {
+            *my_values += *other_values;
+        }
+    }
+}

+ 34 - 22
borg/archive.py

@@ -609,8 +609,9 @@ class ArchiveChecker:
         self.error_found = False
         self.error_found = False
         self.possibly_superseded = set()
         self.possibly_superseded = set()
 
 
-    def check(self, repository, repair=False, last=None):
+    def check(self, repository, repair=False, archive=None, last=None):
         self.report_progress('Starting archive consistency check...')
         self.report_progress('Starting archive consistency check...')
+        self.check_all = archive is None and last is None
         self.repair = repair
         self.repair = repair
         self.repository = repository
         self.repository = repository
         self.init_chunks()
         self.init_chunks()
@@ -619,11 +620,9 @@ class ArchiveChecker:
             self.manifest = self.rebuild_manifest()
             self.manifest = self.rebuild_manifest()
         else:
         else:
             self.manifest, _ = Manifest.load(repository, key=self.key)
             self.manifest, _ = Manifest.load(repository, key=self.key)
-        self.rebuild_refcounts(last=last)
-        if last is None:
-            self.verify_chunks()
-        else:
-            self.report_progress('Orphaned objects check skipped (needs all archives checked)')
+        self.rebuild_refcounts(archive=archive, last=last)
+        self.orphan_chunks_check()
+        self.finish()
         if not self.error_found:
         if not self.error_found:
             self.report_progress('Archive consistency check complete, no problems found.')
             self.report_progress('Archive consistency check complete, no problems found.')
         return self.repair or not self.error_found
         return self.repair or not self.error_found
@@ -631,7 +630,7 @@ class ArchiveChecker:
     def init_chunks(self):
     def init_chunks(self):
         """Fetch a list of all object keys from repository
         """Fetch a list of all object keys from repository
         """
         """
-        # Explicity set the initial hash table capacity to avoid performance issues
+        # Explicitly set the initial hash table capacity to avoid performance issues
         # due to hash table "resonance"
         # due to hash table "resonance"
         capacity = int(len(self.repository) * 1.2)
         capacity = int(len(self.repository) * 1.2)
         self.chunks = ChunkIndex(capacity)
         self.chunks = ChunkIndex(capacity)
@@ -680,7 +679,7 @@ class ArchiveChecker:
         self.report_progress('Manifest rebuild complete', error=True)
         self.report_progress('Manifest rebuild complete', error=True)
         return manifest
         return manifest
 
 
-    def rebuild_refcounts(self, last=None):
+    def rebuild_refcounts(self, archive=None, last=None):
         """Rebuild object reference counts by walking the metadata
         """Rebuild object reference counts by walking the metadata
 
 
         Missing and/or incorrect data is repaired when detected
         Missing and/or incorrect data is repaired when detected
@@ -762,10 +761,17 @@ class ArchiveChecker:
                         yield item
                         yield item
 
 
         repository = cache_if_remote(self.repository)
         repository = cache_if_remote(self.repository)
-        num_archives = len(self.manifest.archives)
-        archive_items = sorted(self.manifest.archives.items(), reverse=True,
-                               key=lambda name_info: name_info[1][b'time'])
-        end = None if last is None else min(num_archives, last)
+        if archive is None:
+            # we need last N or all archives
+            archive_items = sorted(self.manifest.archives.items(), reverse=True,
+                                   key=lambda name_info: name_info[1][b'time'])
+            num_archives = len(self.manifest.archives)
+            end = None if last is None else min(num_archives, last)
+        else:
+            # we only want one specific archive
+            archive_items = [item for item in self.manifest.archives.items() if item[0] == archive]
+            num_archives = 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))
             self.report_progress('Analyzing archive {} ({}/{})'.format(name, num_archives - i, num_archives))
             archive_id = info[b'id']
             archive_id = info[b'id']
@@ -796,16 +802,22 @@ class ArchiveChecker:
             add_reference(new_archive_id, len(data), len(cdata), cdata)
             add_reference(new_archive_id, len(data), len(cdata), cdata)
             info[b'id'] = new_archive_id
             info[b'id'] = new_archive_id
 
 
-    def verify_chunks(self):
-        unused = set()
-        for id_, (count, size, csize) in self.chunks.iteritems():
-            if count == 0:
-                unused.add(id_)
-        orphaned = unused - self.possibly_superseded
-        if orphaned:
-            self.report_progress('{} orphaned objects found'.format(len(orphaned)), error=True)
+    def orphan_chunks_check(self):
+        if self.check_all:
+            unused = set()
+            for id_, (count, size, csize) in self.chunks.iteritems():
+                if count == 0:
+                    unused.add(id_)
+            orphaned = unused - self.possibly_superseded
+            if orphaned:
+                self.report_progress('{} orphaned objects found'.format(len(orphaned)), error=True)
+            if self.repair:
+                for id_ in unused:
+                    self.repository.delete(id_)
+        else:
+            self.report_progress('Orphaned objects check skipped (needs all archives checked)')
+
+    def finish(self):
         if self.repair:
         if self.repair:
-            for id_ in unused:
-                self.repository.delete(id_)
             self.manifest.write()
             self.manifest.write()
             self.repository.commit()
             self.repository.commit()

+ 51 - 17
borg/archiver.py

@@ -86,8 +86,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                 print('Repository check complete, no problems found.')
                 print('Repository check complete, no problems found.')
             else:
             else:
                 return 1
                 return 1
-        if not args.repo_only and not ArchiveChecker().check(repository, repair=args.repair, last=args.last):
-                return 1
+        if not args.repo_only and not ArchiveChecker().check(
+                repository, repair=args.repair, archive=args.repository.archive, last=args.last):
+            return 1
         return 0
         return 0
 
 
     def do_change_passphrase(self, args):
     def do_change_passphrase(self, args):
@@ -223,7 +224,6 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         # 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.')
             print('Warning: File system encoding is "ascii", extracting non-ascii filenames will not be supported.')
-        os.umask(0o077)
         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,
@@ -513,7 +513,12 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
         common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                                    default=False,
                                    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')
+        common_parser.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=0o077, metavar='M',
+                                   help='set umask to M (local and remote, default: 0o077)')
+        common_parser.add_argument('--remote-path', dest='remote_path', default='borg', metavar='PATH',
+                                   help='set remote path to executable (default: "borg")')
 
 
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
         if args:
         if args:
@@ -535,6 +540,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
         This command initializes an empty repository. A repository is a filesystem
         This command initializes an empty repository. A repository is a filesystem
         directory containing the deduplicated data from zero or more archives.
         directory containing the deduplicated data from zero or more archives.
         Encryption can be enabled at repository init time.
         Encryption can be enabled at repository init time.
+        Please note that the 'passphrase' encryption mode is DEPRECATED (instead of it,
+        consider using 'repokey').
         """)
         """)
         subparser = subparsers.add_parser('init', parents=[common_parser],
         subparser = subparsers.add_parser('init', parents=[common_parser],
                                           description=self.do_init.__doc__, epilog=init_epilog,
                                           description=self.do_init.__doc__, epilog=init_epilog,
@@ -544,27 +551,51 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
                                type=location_validator(archive=False),
                                type=location_validator(archive=False),
                                help='repository to create')
                                help='repository to create')
         subparser.add_argument('-e', '--encryption', dest='encryption',
         subparser.add_argument('-e', '--encryption', dest='encryption',
-                               choices=('none', 'passphrase', 'keyfile', 'repokey'), default='none',
-                               help='select encryption method')
+                               choices=('none', 'keyfile', 'repokey', 'passphrase'), default='none',
+                               help='select encryption key mode')
 
 
         check_epilog = textwrap.dedent("""
         check_epilog = textwrap.dedent("""
-        The check command verifies the consistency of a repository and the corresponding
-        archives. The underlying repository data files are first checked to detect bit rot
-        and other types of damage. After that the consistency and correctness of the archive
-        metadata is verified.
-
-        The archive metadata checks can be time consuming and requires access to the key
-        file and/or passphrase if encryption is enabled. These checks can be skipped using
-        the --repository-only option.
+        The check command verifies the consistency of a repository and the corresponding archives.
+
+        First, the underlying repository data files are checked:
+        - For all segments the segment magic (header) is checked
+        - For all objects stored in the segments, all metadata (e.g. crc and size) and
+          all data is read. The read data is checked by size and CRC. Bit rot and other
+          types of accidental damage can be detected this way.
+        - If we are in repair mode and a integrity error is detected for a segment,
+          we try to recover as many objects from the segment as possible.
+        - In repair mode, it makes sure that the index is consistent with the data
+          stored in the segments.
+        - If you use a remote repo server via ssh:, the repo check is executed on the
+          repo server without causing significant network traffic.
+        - The repository check can be skipped using the --archives-only option.
+
+        Second, the consistency and correctness of the archive metadata is verified:
+        - Is the repo manifest present? If not, it is rebuilt from archive metadata
+          chunks (this requires reading and decrypting of all metadata and data).
+        - Check if archive metadata chunk is present. if not, remove archive from
+          manifest.
+        - For all files (items) in the archive, for all chunks referenced by these
+          files, check if chunk is present (if not and we are in repair mode, replace
+          it with a same-size chunk of zeros). This requires reading of archive and
+          file metadata, but not data.
+        - If we are in repair mode and we checked all the archives: delete orphaned
+          chunks from the repo.
+        - if you use a remote repo server via ssh:, the archive check is executed on
+          the client machine (because if encryption is enabled, the checks will require
+          decryption and this is always done client-side, because key access will be
+          required).
+        - The archive checks can be time consuming, they can be skipped using the
+          --repository-only option.
         """)
         """)
         subparser = subparsers.add_parser('check', parents=[common_parser],
         subparser = subparsers.add_parser('check', parents=[common_parser],
                                           description=self.do_check.__doc__,
                                           description=self.do_check.__doc__,
                                           epilog=check_epilog,
                                           epilog=check_epilog,
                                           formatter_class=argparse.RawDescriptionHelpFormatter)
                                           formatter_class=argparse.RawDescriptionHelpFormatter)
         subparser.set_defaults(func=self.do_check)
         subparser.set_defaults(func=self.do_check)
-        subparser.add_argument('repository', metavar='REPOSITORY',
-                               type=location_validator(archive=False),
-                               help='repository to check consistency of')
+        subparser.add_argument('repository', metavar='REPOSITORY_OR_ARCHIVE',
+                               type=location_validator(),
+                               help='repository or archive to check consistency of')
         subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
         subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
                                default=False,
                                default=False,
                                help='only perform repository checks')
                                help='only perform repository checks')
@@ -833,6 +864,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
 
 
         args = parser.parse_args(args or ['-h'])
         args = parser.parse_args(args or ['-h'])
         self.verbose = args.verbose
         self.verbose = args.verbose
+        os.umask(args.umask)
+        RemoteRepository.remote_path = args.remote_path
+        RemoteRepository.umask = args.umask
         update_excludes(args)
         update_excludes(args)
         return args.func(args)
         return args.func(args)
 
 

+ 5 - 2
borg/cache.py

@@ -306,11 +306,14 @@ class Cache:
             chunk_idx.clear()
             chunk_idx.clear()
             for tarinfo in tf_in:
             for tarinfo in tf_in:
                 archive_id_hex = tarinfo.name
                 archive_id_hex = tarinfo.name
+                archive_name = tarinfo.pax_headers['archive_name']
+                print("- extracting archive %s ..." % archive_name)
                 tf_in.extract(archive_id_hex, tmp_dir)
                 tf_in.extract(archive_id_hex, tmp_dir)
                 chunk_idx_path = os.path.join(tmp_dir, archive_id_hex).encode('utf-8')
                 chunk_idx_path = os.path.join(tmp_dir, archive_id_hex).encode('utf-8')
+                print("- reading archive ...")
                 archive_chunk_idx = ChunkIndex.read(chunk_idx_path)
                 archive_chunk_idx = ChunkIndex.read(chunk_idx_path)
-                for chunk_id, (count, size, csize) in archive_chunk_idx.iteritems():
-                    add(chunk_idx, chunk_id, size, csize, incr=count)
+                print("- merging archive ...")
+                chunk_idx.merge(archive_chunk_idx)
                 os.unlink(chunk_idx_path)
                 os.unlink(chunk_idx_path)
 
 
         self.begin_txn()
         self.begin_txn()

+ 10 - 3
borg/hashindex.pyx

@@ -14,6 +14,7 @@ cdef extern from "_hashindex.c":
     void hashindex_summarize(HashIndex *index, long long *total_size, long long *total_csize,
     void hashindex_summarize(HashIndex *index, long long *total_size, long long *total_csize,
                              long long *unique_size, long long *unique_csize,
                              long long *unique_size, long long *unique_csize,
                              long long *total_unique_chunks, long long *total_chunks)
                              long long *total_unique_chunks, long long *total_chunks)
+    void hashindex_merge(HashIndex *index, HashIndex *other)
     int hashindex_get_size(HashIndex *index)
     int hashindex_get_size(HashIndex *index)
     int hashindex_write(HashIndex *index, char *path)
     int hashindex_write(HashIndex *index, char *path)
     void *hashindex_get(HashIndex *index, void *key)
     void *hashindex_get(HashIndex *index, void *key)
@@ -24,15 +25,18 @@ cdef extern from "_hashindex.c":
     int _le32toh(int v)
     int _le32toh(int v)
 
 
 
 
-_NoDefault = object()
+cdef _NoDefault = object()
 
 
+cimport cython
+
+@cython.internal
 cdef class IndexBase:
 cdef class IndexBase:
     cdef HashIndex *index
     cdef HashIndex *index
     key_size = 32
     key_size = 32
 
 
     def __cinit__(self, capacity=0, path=None):
     def __cinit__(self, capacity=0, path=None):
         if path:
         if path:
-            self.index = hashindex_read(<bytes>os.fsencode(path))
+            self.index = hashindex_read(os.fsencode(path))
             if not self.index:
             if not self.index:
                 raise Exception('hashindex_read failed')
                 raise Exception('hashindex_read failed')
         else:
         else:
@@ -49,7 +53,7 @@ cdef class IndexBase:
         return cls(path=path)
         return cls(path=path)
 
 
     def write(self, path):
     def write(self, path):
-        if not hashindex_write(self.index, <bytes>os.fsencode(path)):
+        if not hashindex_write(self.index, os.fsencode(path)):
             raise Exception('hashindex_write failed')
             raise Exception('hashindex_write failed')
 
 
     def clear(self):
     def clear(self):
@@ -187,6 +191,9 @@ cdef class ChunkIndex(IndexBase):
                             &total_unique_chunks, &total_chunks)
                             &total_unique_chunks, &total_chunks)
         return total_size, total_csize, unique_size, unique_csize, total_unique_chunks, total_chunks
         return total_size, total_csize, unique_size, unique_csize, total_unique_chunks, total_chunks
 
 
+    def merge(self, ChunkIndex other):
+        hashindex_merge(self.index, other.index)
+
 
 
 cdef class ChunkKeyIterator:
 cdef class ChunkKeyIterator:
     cdef ChunkIndex idx
     cdef ChunkIndex idx

+ 6 - 3
borg/remote.py

@@ -108,9 +108,10 @@ class RepositoryServer:
 
 
 class RemoteRepository:
 class RemoteRepository:
     extra_test_args = []
     extra_test_args = []
+    remote_path = None
+    umask = None
 
 
     class RPCError(Exception):
     class RPCError(Exception):
-
         def __init__(self, name):
         def __init__(self, name):
             self.name = name
             self.name = name
 
 
@@ -124,8 +125,10 @@ class RemoteRepository:
         self.responses = {}
         self.responses = {}
         self.unpacker = msgpack.Unpacker(use_list=False)
         self.unpacker = msgpack.Unpacker(use_list=False)
         self.p = None
         self.p = None
+        # use local umask also for the remote process
+        umask = ['--umask', '%03o' % self.umask]
         if location.host == '__testsuite__':
         if location.host == '__testsuite__':
-            args = [sys.executable, '-m', 'borg.archiver', 'serve'] + self.extra_test_args
+            args = [sys.executable, '-m', 'borg.archiver', 'serve'] + umask + self.extra_test_args
         else:
         else:
             args = ['ssh']
             args = ['ssh']
             if location.port:
             if location.port:
@@ -134,7 +137,7 @@ class RemoteRepository:
                 args.append('%s@%s' % (location.user, location.host))
                 args.append('%s@%s' % (location.user, location.host))
             else:
             else:
                 args.append('%s' % location.host)
                 args.append('%s' % location.host)
-            args += ['borg', 'serve']
+            args += [self.remote_path, 'serve'] + umask
         self.p = Popen(args, bufsize=0, stdin=PIPE, stdout=PIPE)
         self.p = Popen(args, bufsize=0, stdin=PIPE, stdout=PIPE)
         self.stdin_fd = self.p.stdin.fileno()
         self.stdin_fd = self.p.stdin.fileno()
         self.stdout_fd = self.p.stdout.fileno()
         self.stdout_fd = self.p.stdout.fileno()

+ 1 - 26
borg/testsuite/__init__.py

@@ -73,7 +73,7 @@ class BaseTestCase(unittest.TestCase):
             d1 = [filename] + [getattr(s1, a) for a in attrs]
             d1 = [filename] + [getattr(s1, a) for a in attrs]
             d2 = [filename] + [getattr(s2, a) for a in attrs]
             d2 = [filename] + [getattr(s2, a) for a in attrs]
             if not os.path.islink(path1) or utime_supports_fd:
             if not os.path.islink(path1) or utime_supports_fd:
-                # Older versions of llfuse does not support ns precision properly
+                # Older versions of llfuse do not support ns precision properly
                 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))
@@ -94,28 +94,3 @@ class BaseTestCase(unittest.TestCase):
                 return
                 return
             time.sleep(.1)
             time.sleep(.1)
         raise Exception('wait_for_mount(%s) timeout' % path)
         raise Exception('wait_for_mount(%s) timeout' % path)
-
-
-def get_tests(suite):
-    """Generates a sequence of tests from a test suite
-    """
-    for item in suite:
-        try:
-            # TODO: This could be "yield from..." with Python 3.3+
-            for i in get_tests(item):
-                yield i
-        except TypeError:
-            yield item
-
-
-class TestLoader(unittest.TestLoader):
-    """A customized test loader that properly detects and filters our test cases
-    """
-
-    def loadTestsFromName(self, pattern, module=None):
-        suite = self.discover('borg.testsuite', '*.py')
-        tests = unittest.TestSuite()
-        for test in get_tests(suite):
-            if pattern.lower() in test.id().lower():
-                tests.addTest(test)
-        return tests

+ 1 - 1
borg/testsuite/archive.py

@@ -1,12 +1,12 @@
 from datetime import datetime, timezone
 from datetime import datetime, timezone
 
 
 import msgpack
 import msgpack
+from mock import Mock
 
 
 from ..archive import Archive, CacheChunkBuffer, RobustUnpacker
 from ..archive import Archive, CacheChunkBuffer, RobustUnpacker
 from ..key import PlaintextKey
 from ..key import PlaintextKey
 from ..helpers import Manifest
 from ..helpers import Manifest
 from . import BaseTestCase
 from . import BaseTestCase
-from .mock import Mock
 
 
 
 
 class MockCache:
 class MockCache:

+ 22 - 1
borg/testsuite/archiver.py

@@ -11,6 +11,8 @@ import time
 import unittest
 import unittest
 from hashlib import sha256
 from hashlib import sha256
 
 
+from mock import patch
+
 from .. import xattr
 from .. import xattr
 from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
 from ..archive import Archive, ChunkBuffer, CHUNK_MAX_EXP
 from ..archiver import Archiver
 from ..archiver import Archiver
@@ -20,7 +22,6 @@ from ..helpers import Manifest
 from ..remote import RemoteRepository, PathNotAllowed
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
 from ..repository import Repository
 from . import BaseTestCase
 from . import BaseTestCase
-from .mock import patch
 
 
 try:
 try:
     import llfuse
     import llfuse
@@ -243,6 +244,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         if sparse_support and hasattr(st, 'st_blocks'):
         if sparse_support and hasattr(st, 'st_blocks'):
             self.assert_true(st.st_blocks * 512 < total_len / 10)  # is output sparse?
             self.assert_true(st.st_blocks * 512 < total_len / 10)  # is output sparse?
 
 
+    def test_unusual_filenames(self):
+        filenames = ['normal', 'with some blanks', '(with_parens)', ]
+        for filename in filenames:
+            filename = os.path.join(self.input_path, filename)
+            with open(filename, 'wb') as fd:
+                pass
+        self.cmd('init', self.repository_location)
+        self.cmd('create', self.repository_location + '::test', 'input')
+        for filename in filenames:
+            with changedir('output'):
+                self.cmd('extract', self.repository_location + '::test', os.path.join('input', filename))
+            assert os.path.exists(os.path.join('output', 'input', filename))
+
     def test_repository_swap_detection(self):
     def test_repository_swap_detection(self):
         self.create_test_files()
         self.create_test_files()
         os.environ['BORG_PASSPHRASE'] = 'passphrase'
         os.environ['BORG_PASSPHRASE'] = 'passphrase'
@@ -425,6 +439,13 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             # Restore permissions so shutil.rmtree is able to delete it
             # Restore permissions so shutil.rmtree is able to delete it
             os.system('chmod -R u+w ' + self.repository_path)
             os.system('chmod -R u+w ' + self.repository_path)
 
 
+    def test_umask(self):
+        self.create_regular_file('file1', size=1024 * 80)
+        self.cmd('init', self.repository_location)
+        self.cmd('create', self.repository_location + '::test', 'input')
+        mode = os.stat(self.repository_path).st_mode
+        self.assertEqual(stat.S_IMODE(mode), 0o700)
+
     def test_cmdline_compatibility(self):
     def test_cmdline_compatibility(self):
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('file1', size=1024 * 80)
         self.cmd('init', self.repository_location)
         self.cmd('init', self.repository_location)

+ 22 - 0
borg/testsuite/hashindex.py

@@ -6,6 +6,11 @@ from ..hashindex import NSIndex, ChunkIndex
 from . import BaseTestCase
 from . import BaseTestCase
 
 
 
 
+def H(x):
+    # make some 32byte long thing that depends on x
+    return bytes('%-0.32d' % x, 'ascii')
+
+
 class HashIndexTestCase(BaseTestCase):
 class HashIndexTestCase(BaseTestCase):
 
 
     def _generic_test(self, cls, make_value, sha):
     def _generic_test(self, cls, make_value, sha):
@@ -78,3 +83,20 @@ class HashIndexTestCase(BaseTestCase):
         second_half = list(idx.iteritems(marker=all[49][0]))
         second_half = list(idx.iteritems(marker=all[49][0]))
         self.assert_equal(len(second_half), 50)
         self.assert_equal(len(second_half), 50)
         self.assert_equal(second_half, all[50:])
         self.assert_equal(second_half, all[50:])
+
+    def test_chunkindex_merge(self):
+        idx1 = ChunkIndex()
+        idx1[H(1)] = 1, 100, 100
+        idx1[H(2)] = 2, 200, 200
+        idx1[H(3)] = 3, 300, 300
+        # no H(4) entry
+        idx2 = ChunkIndex()
+        idx2[H(1)] = 4, 100, 100
+        idx2[H(2)] = 5, 200, 200
+        # no H(3) entry
+        idx2[H(4)] = 6, 400, 400
+        idx1.merge(idx2)
+        assert idx1[H(1)] == (5, 100, 100)
+        assert idx1[H(2)] == (7, 200, 200)
+        assert idx1[H(3)] == (3, 300, 300)
+        assert idx1[H(4)] == (6, 400, 400)

+ 0 - 14
borg/testsuite/mock.py

@@ -1,14 +0,0 @@
-"""
-Mocking
-
-Note: unittest.mock is broken on at least python 3.3.6 and 3.4.0.
-      it silently ignores mistyped method names starting with assert_...,
-      does nothing and just succeeds.
-      The issue was fixed in the separately distributed "mock" lib, you
-      get an AttributeError there. So, always use that one!
-
-Details:
-
-http://engineeringblog.yelp.com/2015/02/assert_called_once-threat-or-menace.html
-"""
-from mock import *

+ 2 - 1
borg/testsuite/repository.py

@@ -2,13 +2,14 @@ import os
 import shutil
 import shutil
 import tempfile
 import tempfile
 
 
+from mock import patch
+
 from ..hashindex import NSIndex
 from ..hashindex import NSIndex
 from ..helpers import Location, IntegrityError
 from ..helpers import Location, IntegrityError
 from ..locking import UpgradableLock
 from ..locking import UpgradableLock
 from ..remote import RemoteRepository, InvalidRPCMethod
 from ..remote import RemoteRepository, InvalidRPCMethod
 from ..repository import Repository
 from ..repository import Repository
 from . import BaseTestCase
 from . import BaseTestCase
-from .mock import patch
 
 
 
 
 class RepositoryTestCaseBase(BaseTestCase):
 class RepositoryTestCaseBase(BaseTestCase):

+ 0 - 11
borg/testsuite/run.py

@@ -1,11 +0,0 @@
-import unittest
-
-from . import TestLoader
-
-
-def main():
-    unittest.main(testLoader=TestLoader(), defaultTest='')
-
-
-if __name__ == '__main__':
-    main()

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

@@ -5,6 +5,8 @@
 <ul>
 <ul>
   <li><a href="https://borgbackup.github.io/borgbackup/">Main Web Site</a></li>
   <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://pypi.python.org/pypi/borgbackup">PyPI packages</a></li>
+  <li><a href="https://github.com/borgbackup/borg/issues/147">Binary Packages</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">GitHub</a></li>
   <li><a href="https://github.com/borgbackup/borg/issues">Issue Tracker</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="https://www.bountysource.com/teams/borgbackup">Bounties &amp; Fundraisers</a></li>

+ 4 - 0
docs/changes.rst

@@ -0,0 +1,4 @@
+.. include:: global.rst.inc
+.. _changelog:
+
+.. include:: ../CHANGES.rst

+ 6 - 6
docs/conf.py

@@ -11,13 +11,13 @@
 # All configuration values have a default; values that are commented out
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 # serve to show the default.
 
 
-from borg import __version__ as sw_version
-
 # If extensions (or modules to document with autodoc) are in another directory,
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-#import sys, os
-#sys.path.insert(0, os.path.abspath('.'))
+import sys, os
+sys.path.insert(0, os.path.abspath('..'))
+
+from borg import __version__ as sw_version
 
 
 # -- General configuration -----------------------------------------------------
 # -- General configuration -----------------------------------------------------
 
 
@@ -42,7 +42,7 @@ master_doc = 'index'
 
 
 # General information about the project.
 # General information about the project.
 project = 'Borg - Deduplicating Archiver'
 project = 'Borg - Deduplicating Archiver'
-copyright = '2010-2014, Jonas Borgström'
+copyright = '2010-2014, Jonas Borgström, 2015 The Borg Collective (see AUTHORS file)'
 
 
 # The version info for the project you're documenting, acts as replacement for
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # |version| and |release|, also used in various other places throughout the
@@ -134,7 +134,7 @@ html_static_path = []
 # Custom sidebar templates, maps document names to template names.
 # Custom sidebar templates, maps document names to template names.
 html_sidebars = {
 html_sidebars = {
     'index': ['sidebarlogo.html', 'sidebarusefullinks.html', 'searchbox.html'],
     'index': ['sidebarlogo.html', 'sidebarusefullinks.html', 'searchbox.html'],
-    '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 'sidebarusefullinks.html', 'searchbox.html']
+    '**': ['sidebarlogo.html', 'relations.html', 'searchbox.html', 'localtoc.html', 'sidebarusefullinks.html']
 }
 }
 # Additional templates that should be rendered to pages, maps page names to
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
 # template names.

+ 67 - 0
docs/development.rst

@@ -0,0 +1,67 @@
+.. include:: global.rst.inc
+.. _development:
+
+Development
+===========
+
+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
+the performance critical parts).
+
+
+Building a development environment
+----------------------------------
+
+First, just install borg into a virtual env as described before.
+
+To install some additional packages needed for running the tests, activate your
+virtual env and run::
+
+  pip install -r requirements.d/development.txt
+
+
+Running the tests
+-----------------
+
+The tests are in the borg/testsuite package.
+
+To run them, you need to have fakeroot, tox and pytest installed.
+
+To run the test suite use the following command::
+
+  fakeroot -u tox  # run all tests
+
+Some more advanced examples::
+
+  # verify a changed tox.ini (run this after any change to tox.ini):
+  fakeroot -u tox --recreate
+
+  fakeroot -u tox -e py32  # run all tests, but only on python 3.2
+
+  fakeroot -u tox borg.testsuite.locking  # only run 1 test module
+
+  fakeroot -u tox borg.testsuite.locking -- -k '"not Timer"'  # exclude some tests
+
+  fakeroot -u tox borg.testsuite -- -v  # verbose py.test
+
+Important notes:
+
+- Without fakeroot -u some tests will fail.
+- When using -- to give options to py.test, you MUST also give borg.testsuite[.module].
+
+Building the docs with Sphinx
+-----------------------------
+
+The documentation (in reStructuredText format, .rst) is in docs/.
+
+To build the html version of it, you need to have sphinx installed::
+
+  pip3 install sphinx
+
+Now run::
+
+  cd docs/
+  make html
+
+Then point a web browser at docs/_build/html/index.html.

+ 0 - 65
docs/foreword.rst

@@ -1,65 +0,0 @@
-.. include:: global.rst.inc
-.. _foreword:
-
-Foreword
-========
-
-|project_name| is a secure backup program for Linux, FreeBSD and Mac OS X. 
-|project_name| is designed for efficient data storage where only new or
-modified data is stored.
-
-Features
---------
-
-Space efficient storage
-    Variable block size `deduplication`_ is used to reduce the number of bytes 
-    stored by detecting redundant data. Each file is split into a number of
-    variable length chunks and only chunks that have never been seen before
-    are added to the repository (and optionally compressed).
-
-Optional data encryption
-    All data can be protected using 256-bit AES_ encryption and data integrity
-    and authenticity is verified using `HMAC-SHA256`_.
-
-Off-site backups
-    |project_name| can store data on any remote host accessible over SSH as
-    long as |project_name| is installed. If you don't have |project_name|
-    installed there, you can use some network filesytem (sshfs, nfs, ...)
-    to mount a filesystem located on your remote host and use it like it was
-    local (but that will be slower).
-
-Backups mountable as filesystems
-    Backup archives are :ref:`mountable <borg_mount>` as
-    `userspace filesystems`_ for easy backup verification and restores.
-
-
-Glossary
---------
-
-.. _deduplication_def:
-
-Deduplication
-    Deduplication is a technique for improving storage utilization by
-    eliminating redundant data. 
-
-.. _archive_def:
-
-Archive
-    An archive is a collection of files along with metadata that include file
-    permissions, directory structure and various file attributes.
-    Since each archive in a repository must have a unique name a good naming
-    convention is ``hostname-YYYY-MM-DD``.
-
-.. _repository_def:
-
-Repository
-    A repository is a filesystem directory storing data from zero or more
-    archives. The data in a repository is both deduplicated and 
-    optionally encrypted making it both efficient and safe. Repositories are
-    created using :ref:`borg_init` and the contents can be listed using
-    :ref:`borg_list`.
-
-Key file
-    When a repository is initialized a key file containing a password
-    protected encryption key is created. It is vital to keep this file safe
-    since the repository data is totally inaccessible without it.

+ 6 - 69
docs/index.rst

@@ -1,81 +1,18 @@
 .. include:: global.rst.inc
 .. include:: global.rst.inc
 
 
-Welcome to Borg
-================
-|project_name| is a deduplicating backup program.
-Optionally, it also supports compression and authenticated encryption.
 
 
-The main goal of |project_name| is to provide an efficient and secure way
-to backup data. The data deduplication technique used makes |project_name|
-suitable for daily backups since only the changes are stored. The authenticated
-encryption makes it suitable for backups to not fully trusted targets.
-
-|project_name| is written in Python (with a little bit of Cython and C for
-the performance critical parts).
-
-
-Easy to use
------------
-Initialize a new backup :ref:`repository <repository_def>` and create your
-first backup :ref:`archive <archive_def>` in two lines::
-
-    $ borg init /mnt/backup
-    $ borg create /mnt/backup::Monday ~/Documents
-    $ borg create --stats /mnt/backup::Tuesday ~/Documents
-    Archive name: Tuesday
-    Archive fingerprint: 387a5e3f9b0e792e91ce87134b0f4bfe17677d9248cb5337f3fbf3a8e157942a
-    Start time: Tue Mar 25 12:00:10 2014
-    End time:   Tue Mar 25 12:00:10 2014
-    Duration: 0.08 seconds
-    Number of files: 358
-                           Original size      Compressed size    Deduplicated size
-    This archive:               57.16 MB             46.78 MB            151.67 kB
-    All archives:              114.02 MB             93.46 MB             44.81 MB
-
-See the :ref:`quickstart` chapter for a more detailed example.
-
-Easy installation
------------------
-You can use pip to install |project_name| quickly and easily::
-
-    $ pip3 install borgbackup
-
-Need more help with installing? See :ref:`installation`.
-
-User's Guide
-============
+Borg Documentation
+==================
 
 
 .. toctree::
 .. toctree::
    :maxdepth: 2
    :maxdepth: 2
 
 
-   foreword
+   intro
    installation
    installation
    quickstart
    quickstart
    usage
    usage
    faq
    faq
+   support
+   changes
    internals
    internals
-
-Getting help
-============
-
-If you've found a bug or have a concrete feature request, please create a new
-ticket on the project's `issue tracker`_ (after checking whether someone else
-already has reported the same thing).
-
-For more general questions or discussions, IRC or mailing list are preferred.
-
-IRC
----
-Join us on channel #borgbackup on chat.freenode.net. As usual on IRC, just
-ask or tell directly and then patiently wait for replies. Stay connected.
-
-Mailing list
-------------
-
-There is a mailing list for Borg on librelist_ that you can use for feature
-requests and general discussions about Borg. A mailing list archive is
-available `here <http://librelist.com/browser/borgbackup/>`_.
-
-To subscribe to the list, send an email to borgbackup@librelist.com and reply
-to the confirmation mail. Likewise, to unsubscribe, send an email to 
-borgbackup-unsubscribe@librelist.com and reply to the confirmation mail.
+   development

+ 7 - 0
docs/intro.rst

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

+ 34 - 0
docs/support.rst

@@ -0,0 +1,34 @@
+.. include:: global.rst.inc
+.. _support:
+
+Support
+=======
+
+Issue Tracker
+-------------
+
+If you've found a bug or have a concrete feature request, please create a new
+ticket on the project's `issue tracker`_ (after checking whether someone else
+already has reported the same thing).
+
+For more general questions or discussions, IRC or mailing list are preferred.
+
+IRC
+---
+Join us on channel #borgbackup on chat.freenode.net.
+
+As usual on IRC, just ask or tell directly and then patiently wait for replies.
+Stay connected.
+
+Mailing list
+------------
+
+There is a mailing list for Borg on librelist_ that you can use for feature
+requests and general discussions about Borg. A mailing list archive is
+available `here <http://librelist.com/browser/borgbackup/>`_.
+
+To subscribe to the list, send an email to borgbackup@librelist.com and reply
+to the confirmation mail.
+
+To unsubscribe, send an email to borgbackup-unsubscribe@librelist.com and reply
+to the confirmation mail.

+ 1 - 0
requirements.d/development.txt

@@ -1,4 +1,5 @@
 tox
 tox
 mock
 mock
 pytest
 pytest
+pytest-cov<2.0.0
 Cython
 Cython

+ 1 - 12
tox.ini

@@ -1,16 +1,5 @@
 # tox configuration - if you change anything here, run this to verify:
 # tox configuration - if you change anything here, run this to verify:
 # fakeroot -u tox --recreate
 # fakeroot -u tox --recreate
-#
-# Invokation examples:
-# fakeroot -u tox  # run all tests
-# fakeroot -u tox -e py32  # run all tests, but only on python 3.2
-# fakeroot -u tox borg.testsuite.locking  # only run 1 test module
-# fakeroot -u tox borg.testsuite.locking -- -k '"not Timer"'  # exclude some tests
-# fakeroot -u tox borg.testsuite -- -v  # verbose py.test
-#
-# Important notes:
-# Without fakeroot -u some tests will fail.
-# When using -- to give options to py.test, you MUST also give borg.testsuite[.module].
 
 
 [tox]
 [tox]
 envlist = py32, py33, py34
 envlist = py32, py33, py34
@@ -20,6 +9,6 @@ envlist = py32, py33, py34
 # not really matter, should be just different from the toplevel dir.
 # not really matter, should be just different from the toplevel dir.
 changedir = {toxworkdir}
 changedir = {toxworkdir}
 deps = -rrequirements.d/development.txt
 deps = -rrequirements.d/development.txt
-commands = py.test --pyargs {posargs:borg.testsuite}
+commands = py.test --cov=borg --pyargs {posargs:borg.testsuite}
 # fakeroot -u needs some env vars:
 # fakeroot -u needs some env vars:
 passenv = *
 passenv = *