~antmak/duplicity/0.7-par2-fix

« back to all changes in this revision

Viewing changes to duplicity/backends/par2backend.py

  • Committer: Kenneth Loafman
  • Date: 2014-04-29 15:35:47 UTC
  • mfrom: (978.2.10 backend-unification)
  • Revision ID: kenneth@loafman.com-20140429153547-to2j1tyyl0ps1hi6
* Merged in lp:~mterry/duplicity/backend-unification
  - Reorganize and simplify backend code.  Specifically:
    - Formalize the expected API between backends and duplicity.  See the new
      file duplicity/backends/README for the instructions I've given authors.
    - Add some tests for our backend wrapper class as well as some tests for
      individual backends.  For several backends that have some commands do all
      the heavy lifting (hsi, tahoe, ftp), I've added fake little mock commands
      so that we can test them locally.  This doesn't truly test our integration
      with those commands, but at least lets us test the backend glue code.
    - Removed a lot of duplicate and unused code which backends were using (or
      not using).  This branch drops 700 lines of code (~20%)
      in duplicity/backends!
    - Simplified expectations of backends.  Our wrapper code now does all the
      retrying, and all the exception handling.  Backends can 'fire and forget'
      trusting our wrappers to give the user a reasonable error message.
      Obviously, backends can also add more details and make nicer error
      messages.  But they don't *have* to.
    - Separate out the backend classes from our wrapper class.  Now there is no
      possibility of namespace collision.  All our API methods use one
      underscore.  Anything else (zero or two underscores) are for the backend
      class's use.
    - Added the concept of a 'backend prefix' which is used by par2 and gio
      backends to provide generic support for "schema+" in urls -- like par2+
      or gio+.  I've since marked the '--gio' flag as deprecated, in favor of
      'gio+'.  Now you can even nest such backends like
      par2+gio+file://blah/blah.
    - The switch to control which cloudfiles backend had a typo.  I fixed this,
      but I'm not sure I should have?  If we haven't had complaints, maybe we
      can just drop the old backend.
    - I manually tested all the backends we have (except hsi and tahoe -- but
      those are simple wrappers around commands and I did test those via mocks
      per above).  I also added a bunch more manual backend tests to
      ./testing/manual/backendtest.py, which can now be run like the above to
      test all the files you have configured in config.py or you can pass it a
      URL which it will use for testing (useful for backend authors).

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
# along with duplicity; if not, write to the Free Software Foundation,
17
17
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
18
 
 
19
from future_builtins import filter
 
20
 
19
21
import os
20
22
import re
21
23
from duplicity import backend
22
 
from duplicity.errors import UnsupportedBackendScheme, BackendException
 
24
from duplicity.errors import BackendException
23
25
from duplicity import log
24
26
from duplicity import globals
25
27
 
26
 
class Par2WrapperBackend(backend.Backend):
 
28
class Par2Backend(backend.Backend):
27
29
    """This backend wrap around other backends and create Par2 recovery files
28
30
    before the file and the Par2 files are transfered with the wrapped backend.
29
31
    
37
39
        except AttributeError:
38
40
            self.redundancy = 10
39
41
 
40
 
        try:
41
 
            url_string = self.parsed_url.url_string.lstrip('par2+')
42
 
            self.wrapped_backend = backend.get_backend(url_string)
43
 
        except:
44
 
            raise UnsupportedBackendScheme(self.parsed_url.url_string)
45
 
 
46
 
    def put(self, source_path, remote_filename = None):
 
42
        self.wrapped_backend = backend.get_backend_object(parsed_url.url_string)
 
43
 
 
44
        for attr in ['_get', '_put', '_list', '_delete', '_delete_list',
 
45
                     '_query', '_query_list', '_retry_cleanup', '_error_code',
 
46
                     '_move', '_close']:
 
47
            if hasattr(self.wrapped_backend, attr):
 
48
                setattr(self, attr, getattr(self, attr[1:]))
 
49
 
 
50
    def transfer(self, method, source_path, remote_filename):
47
51
        """create Par2 files and transfer the given file and the Par2 files
48
52
        with the wrapped backend.
49
53
        
52
56
        the soure_path with remote_filename into this. 
53
57
        """
54
58
        import pexpect
55
 
        if remote_filename is None:
56
 
            remote_filename = source_path.get_filename()
57
59
 
58
60
        par2temp = source_path.get_temp_in_same_dir()
59
61
        par2temp.mkdir()
60
62
        source_symlink = par2temp.append(remote_filename)
61
 
        os.symlink(source_path.get_canonical(), source_symlink.get_canonical())
 
63
        source_target = source_path.get_canonical()
 
64
        if not os.path.isabs(source_target):
 
65
            source_target = os.path.join(os.getcwd(), source_target)
 
66
        os.symlink(source_target, source_symlink.get_canonical())
62
67
        source_symlink.setdata()
63
68
 
64
69
        log.Info("Create Par2 recovery files")
70
75
            for file in par2temp.listdir():
71
76
                files_to_transfer.append(par2temp.append(file))
72
77
 
73
 
        ret = self.wrapped_backend.put(source_path, remote_filename)
 
78
        method(source_path, remote_filename)
74
79
        for file in files_to_transfer:
75
 
            self.wrapped_backend.put(file, file.get_filename())
 
80
            method(file, file.get_filename())
76
81
 
77
82
        par2temp.deltree()
78
 
        return ret
79
 
 
80
 
    def move(self, source_path, remote_filename = None):
81
 
        self.put(source_path, remote_filename)
82
 
        source_path.delete()
 
83
 
 
84
    def put(self, local, remote):
 
85
        self.transfer(self.wrapped_backend._put, local, remote)
 
86
 
 
87
    def move(self, local, remote):
 
88
        self.transfer(self.wrapped_backend._move, local, remote)
83
89
 
84
90
    def get(self, remote_filename, local_path):
85
91
        """transfer remote_filename and the related .par2 file into
94
100
        par2temp.mkdir()
95
101
        local_path_temp = par2temp.append(remote_filename)
96
102
 
97
 
        ret = self.wrapped_backend.get(remote_filename, local_path_temp)
 
103
        self.wrapped_backend._get(remote_filename, local_path_temp)
98
104
 
99
105
        try:
100
106
            par2file = par2temp.append(remote_filename + '.par2')
101
 
            self.wrapped_backend.get(par2file.get_filename(), par2file)
 
107
            self.wrapped_backend._get(par2file.get_filename(), par2file)
102
108
 
103
109
            par2verify = 'par2 v -q -q %s %s' % (par2file.get_canonical(), local_path_temp.get_canonical())
104
110
            out, returncode = pexpect.run(par2verify, -1, True)
105
111
 
106
112
            if returncode:
107
113
                log.Warn("File is corrupt. Try to repair %s" % remote_filename)
108
 
                par2volumes = self.list(re.compile(r'%s\.vol[\d+]*\.par2' % remote_filename))
 
114
                par2volumes = filter(re.compile((r'%s\.vol[\d+]*\.par2' % remote_filename).match,
 
115
                                     self.wrapped_backend._list()))
109
116
 
110
117
                for filename in par2volumes:
111
118
                    file = par2temp.append(filename)
112
 
                    self.wrapped_backend.get(filename, file)
 
119
                    self.wrapped_backend._get(filename, file)
113
120
 
114
121
                par2repair = 'par2 r -q -q %s %s' % (par2file.get_canonical(), local_path_temp.get_canonical())
115
122
                out, returncode = pexpect.run(par2repair, -1, True)
124
131
        finally:
125
132
            local_path_temp.rename(local_path)
126
133
            par2temp.deltree()
127
 
        return ret
128
134
 
129
 
    def list(self, filter = re.compile(r'(?!.*\.par2$)')):
130
 
        """default filter all files that ends with ".par"
131
 
        filter can be a re.compile instance or False for all remote files
 
135
    def delete(self, filename):
 
136
        """delete given filename and its .par2 files
132
137
        """
133
 
        list = self.wrapped_backend.list()
134
 
        if not filter:
135
 
            return list
136
 
        filtered_list = []
137
 
        for item in list:
138
 
            if filter.match(item):
139
 
                filtered_list.append(item)
140
 
        return filtered_list
141
 
 
142
 
    def delete(self, filename_list):
 
138
        self.wrapped_backend._delete(filename)
 
139
 
 
140
        remote_list = self.list()
 
141
        filename_list = [filename]
 
142
        c =  re.compile(r'%s(?:\.vol[\d+]*)?\.par2' % filename)
 
143
        for remote_filename in remote_list:
 
144
            if c.match(remote_filename):
 
145
                self.wrapped_backend._delete(remote_filename)
 
146
 
 
147
    def delete_list(self, filename_list):
143
148
        """delete given filename_list and all .par2 files that belong to them
144
149
        """
145
 
        remote_list = self.list(False)
 
150
        remote_list = self.list()
146
151
 
147
152
        for filename in filename_list[:]:
148
153
            c =  re.compile(r'%s(?:\.vol[\d+]*)?\.par2' % filename)
150
155
                if c.match(remote_filename):
151
156
                    filename_list.append(remote_filename)
152
157
 
153
 
        return self.wrapped_backend.delete(filename_list)
154
 
 
155
 
    """just return the output of coresponding wrapped backend
156
 
    for all other functions
157
 
    """
158
 
    def query_info(self, filename_list, raise_errors=True):
159
 
        return self.wrapped_backend.query_info(filename_list, raise_errors)
160
 
 
161
 
    def get_password(self):
162
 
        return self.wrapped_backend.get_password()
163
 
 
164
 
    def munge_password(self, commandline):
165
 
        return self.wrapped_backend.munge_password(commandline)
166
 
 
167
 
    def run_command(self, commandline):
168
 
        return self.wrapped_backend.run_command(commandline)
169
 
    def run_command_persist(self, commandline):
170
 
        return self.wrapped_backend.run_command_persist(commandline)
171
 
 
172
 
    def popen(self, commandline):
173
 
        return self.wrapped_backend.popen(commandline)
174
 
    def popen_persist(self, commandline):
175
 
        return self.wrapped_backend.popen_persist(commandline)
176
 
 
177
 
    def _subprocess_popen(self, commandline):
178
 
        return self.wrapped_backend._subprocess_popen(commandline)
179
 
 
180
 
    def subprocess_popen(self, commandline):
181
 
        return self.wrapped_backend.subprocess_popen(commandline)
182
 
 
183
 
    def subprocess_popen_persist(self, commandline):
184
 
        return self.wrapped_backend.subprocess_popen_persist(commandline)
 
158
        return self.wrapped_backend._delete_list(filename_list)
 
159
 
 
160
 
 
161
    def list(self):
 
162
        return self.wrapped_backend._list()
 
163
 
 
164
    def retry_cleanup(self):
 
165
        self.wrapped_backend._retry_cleanup()
 
166
 
 
167
    def error_code(self, operation, e):
 
168
        return self.wrapped_backend._error_code(operation, e)
 
169
 
 
170
    def query(self, filename):
 
171
        return self.wrapped_backend._query(filename)
 
172
 
 
173
    def query_list(self, filename_list):
 
174
        return self.wrapped_backend._query(filename_list)
185
175
 
186
176
    def close(self):
187
 
        return self.wrapped_backend.close()
188
 
 
189
 
"""register this backend with leading "par2+" for all already known backends
190
 
 
191
 
files must be sorted in duplicity.backend.import_backends to catch
192
 
all supported backends
193
 
"""
194
 
for item in backend._backends.keys():
195
 
    backend.register_backend('par2+' + item, Par2WrapperBackend)
 
177
        self.wrapped_backend._close()
 
178
 
 
179
backend.register_backend_prefix('par2', Par2Backend)