~duplicity-team/duplicity/0.7-series

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#
# Copyright (c) 2015 Matthew Bentley
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import hashlib

import duplicity.backend
from duplicity.errors import BackendException, FatalBackendException
from duplicity import log
from duplicity import progress


def progress_listener_factory():
    u"""
    Returns progress instance suitable for passing to b2.api.B2Api
    methods.
    """
    class B2ProgressListener(b2.progress.AbstractProgressListener):
        def __init__(self):
            super(B2ProgressListener, self).__init__()

        def set_total_bytes(self, total_byte_count):
            self.total_byte_count = total_byte_count

        def bytes_completed(self, byte_count):
            progress.report_transfer(byte_count, self.total_byte_count)

        def close(self):
            super(B2ProgressListener, self).close()
    return B2ProgressListener()


class B2Backend(duplicity.backend.Backend):
    """
    Backend for BackBlaze's B2 storage service
    """

    def __init__(self, parsed_url):
        """
        Authorize to B2 api and set up needed variables
        """
        duplicity.backend.Backend.__init__(self, parsed_url)

        # Import B2 API
        try:
            global b2
            import b2
            import b2.api
            import b2.account_info
            import b2.download_dest
            import b2.file_version
            import b2.progress
        except ImportError:
            raise BackendException('B2 backend requires B2 Python APIs (pip install b2)')

        self.service = b2.api.B2Api(b2.account_info.InMemoryAccountInfo())
        self.parsed_url.hostname = 'B2'

        account_id = parsed_url.username
        account_key = self.get_password()

        self.url_parts = [
            x for x in parsed_url.path.replace("@", "/").split('/') if x != ''
        ]
        if self.url_parts:
            self.username = self.url_parts.pop(0)
            bucket_name = self.url_parts.pop(0)
        else:
            raise BackendException("B2 requires a bucket name")
        self.path = "".join([url_part + "/" for url_part in self.url_parts])
        self.service.authorize_account('production', account_id, account_key)

        log.Log("B2 Backend (path= %s, bucket= %s, minimum_part_size= %s)" %
                (self.path, bucket_name, self.service.account_info.get_minimum_part_size()), log.INFO)
        try:
            self.bucket = self.service.get_bucket_by_name(bucket_name)
            log.Log("Bucket found", log.INFO)
        except b2.exception.NonExistentBucket:
            try:
                log.Log("Bucket not found, creating one", log.INFO)
                self.bucket = self.service.create_bucket(bucket_name, 'allPrivate')
            except:
                raise FatalBackendException("Bucket cannot be created")

    def _get(self, remote_filename, local_path):
        """
        Download remote_filename to local_path
        """
        log.Log("Get: %s -> %s" % (self.path + remote_filename, local_path.name), log.INFO)
        self.bucket.download_file_by_name(self.path + remote_filename,
                                          b2.download_dest.DownloadDestLocalFile(local_path.name))

    def _put(self, source_path, remote_filename):
        """
        Copy source_path to remote_filename
        """
        log.Log("Put: %s -> %s" % (source_path.name, self.path + remote_filename), log.INFO)
        self.bucket.upload_local_file(source_path.name, self.path + remote_filename,
                                      content_type='application/pgp-encrypted',
                                      progress_listener=progress_listener_factory())

    def _list(self):
        """
        List files on remote server
        """
        return [file_version_info.file_name[len(self.path):]
                for (file_version_info, folder_name) in self.bucket.ls(self.path)]

    def _delete(self, filename):
        """
        Delete filename from remote server
        """
        log.Log("Delete: %s" % self.path + filename, log.INFO)
        file_version_info = self.file_info(self.path + filename)
        self.bucket.delete_file_version(file_version_info.id_, file_version_info.file_name)

    def _query(self, filename):
        """
        Get size info of filename
        """
        log.Log("Query: %s" % self.path + filename, log.INFO)
        file_version_info = self.file_info(self.path + filename)
        return {'size': file_version_info.size
                if file_version_info is not None and file_version_info.size is not None else -1}

    def file_info(self, filename):
        response = self.bucket.list_file_names(filename, 1)
        for entry in response['files']:
            file_version_info = b2.file_version.FileVersionInfoFactory.from_api_response(entry)
            if file_version_info.file_name == filename:
                return file_version_info
        raise BackendException('File not found')


duplicity.backend.register_backend("b2", B2Backend)