Merge lp:~prateek/duplicity/s3-glacier into lp:duplicity/0.6

Proposed by someone1
Status: Merged
Merged at revision: 963
Proposed branch: lp:~prateek/duplicity/s3-glacier
Merge into: lp:duplicity/0.6
Diff against target: 998 lines (+321/-416)
8 files modified
README (+1/-0)
bin/duplicity (+11/-0)
bin/duplicity.1 (+38/-0)
duplicity/backends/_boto_multi.py (+75/-308)
duplicity/backends/_boto_single.py (+171/-104)
duplicity/backends/botobackend.py (+11/-4)
duplicity/commandline.py (+8/-0)
duplicity/globals.py (+6/-0)
To merge this branch: bzr merge lp:~prateek/duplicity/s3-glacier
Reviewer Review Type Date Requested Status
edso Needs Information
Review via email: mp+207719@code.launchpad.net

Commit message

Fixes https://bugs.launchpad.net/duplicity/+bug/1039511 - Adds support to detect when a file is on Glacier and initiates a restore to S3. Also merges overlapping code in the boto backends
Fixes https://bugs.launchpad.net/duplicity/+bug/1243246 - Adds a --s3_multipart_max_timeout input option to limit the max execution time of a chunked upload to S3. Also adds debug message to calculate upload speed.

Description of the change

How this addresses bug 1039511:
If a file located in S3 is found to be on Glacier, it will initiate a restore to S3 and wait until the file is ready to continue the restoration process.

This branch also merged _boto_single and _boto_multi as a majority of the code overlaps, so to make updates easier, having _boto_multi as a subclass to _boto_single makes it so changes to shared code is only done in one place.

To post a comment you must log in.
Revision history for this message
edso (ed.so) wrote :

Prateek,

please add your new switches to bin/dupliciy.1 manpage.
also update the requirements documentation in there that you updated in Readme or changed for the backend.

aside from that i assume you extensively tested the changes?

..ede/duply.net

review: Needs Fixing
Revision history for this message
someone1 (prateek) wrote :

I can add the additions to the manpage document. I have been using my branch for backups for 2-3 months now. I can do restores from Glacier without issue. The addition of the Google Storage backend was not tested but I have no reason to think my changes should break it as it utilizes the same boto API as the S3 backend. I am not sure what other forms of testing you'd like me to try out.

My backup job runs a full backup every 28 days and incremental backups in between. I have about 300GB of data I backup.

lp:~prateek/duplicity/s3-glacier updated
933. By someone1

Updated manpage and tweaked boto backend connection reset

Revision history for this message
someone1 (prateek) wrote :

I updated the manpage accordingly and added entries for the other undocumented S3 options (one of which I added myself in a patch submitted years ago). I also noticed that boto has been caching connections in the storage_uri object which was not being cleared out when resetting the S3 connection. I've modified this code and will begin testing it across 3 production systems I manage.

Let me know what you think.

Revision history for this message
edso (ed.so) wrote :

looks good to me and sounds even better (the testing part :).. thx ede/duply.net

review: Approve
lp:~prateek/duplicity/s3-glacier updated
934. By someone1

Make sure each process in a multipart upload get their own fresh connection

Revision history for this message
someone1 (prateek) wrote :

There is an import error, I will push an update to this branch to fix it.

Revision history for this message
someone1 (prateek) wrote :

I meant to push this up immediately but I thought I try a test upload first. It ran through fine, here is the fix: http://bazaar.launchpad.net/~prateek/duplicity/s3-glacier/revision/935

Do you prefer I put in another Merge request or just one off this separately?

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote :

Will just merge it in separately.

On Wed, Feb 26, 2014 at 3:08 PM, someone1 <email address hidden> wrote:

> I meant to push this up immediately but I thought I try a test upload
> first. It ran through fine, here is the fix:
> http://bazaar.launchpad.net/~prateek/duplicity/s3-glacier/revision/935
>
> Do you prefer I put in another Merge request or just one off this
> separately?
> --
> https://code.launchpad.net/~prateek/duplicity/s3-glacier/+merge/207719
> You are subscribed to branch lp:duplicity.
>

Revision history for this message
edso (ed.so) wrote :

On 26.02.2014 22:08, someone1 wrote:
> I meant to push this up immediately but I thought I try a test upload first. It ran through fine, here is the fix: http://bazaar.launchpad.net/~prateek/duplicity/s3-glacier/revision/935
>
> Do you prefer I put in another Merge request or just one off this separately?
>

could you please check how duplicity behaves after that. lazy imports in _init are intentionally there to circumvent import errors during initial backend imports.

just check how duplicity behaves when not having boto avail and using a different backend, say file:// for simplicity. it shouldn't complain about missing boto in that case.

..ede/duply.net

Revision history for this message
edso (ed.so) wrote :

Prateek.. anay news on the above? ..ede

review: Needs Information
Revision history for this message
someone1 (prateek) wrote :

I got the following error:
Import of duplicity.backends.botobackend Failed: No module named boto

I've updated my branch with lazy imports.

On Wed, Mar 5, 2014 at 12:13 PM, edso <email address hidden> wrote:

> Review: Needs Information
>
> Prateek.. anay news on the above? ..ede
> --
> https://code.launchpad.net/~prateek/duplicity/s3-glacier/+merge/207719
> You are the owner of lp:~prateek/duplicity/s3-glacier.
>

Revision history for this message
someone1 (prateek) wrote :

I will put a merge request in for the import fix after I perform a backup tonight and make sure there are no further issues.

As an FYI - a full backup failed last week due to insufficient space on my server at one of the sites I support. There was a successful full backup at another site I manage.

Incremental backups have been going smoothly at all 3 sites I use with the changes I submitted.

Restoration from S3/Glacier have been working as well.

Revision history for this message
edso (ed.so) wrote :

unfortunately that was already merged.. please create a branch against trunk for Ken to merge.

thanks for all your efforts.. ede/duply.net

On 05.03.2014 18:54, someone1 wrote:
> I got the following error:
> Import of duplicity.backends.botobackend Failed: No module named boto
>
> I've updated my branch with lazy imports.
>
>
> On Wed, Mar 5, 2014 at 12:13 PM, edso <email address hidden> wrote:
>
>> Review: Needs Information
>>
>> Prateek.. anay news on the above? ..ede
>> --
>> https://code.launchpad.net/~prateek/duplicity/s3-glacier/+merge/207719
>> You are the owner of lp:~prateek/duplicity/s3-glacier.
>>
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README'
--- README 2014-01-24 12:39:40 +0000
+++ README 2014-02-26 19:49:10 +0000
@@ -29,6 +29,7 @@
29 * Boto 2.0 or later for single-processing S3 or GCS access (default)29 * Boto 2.0 or later for single-processing S3 or GCS access (default)
30 * Boto 2.1.1 or later for multi-processing S3 access30 * Boto 2.1.1 or later for multi-processing S3 access
31 * Python v2.6 or later for multi-processing S3 access31 * Python v2.6 or later for multi-processing S3 access
32 * Boto 2.7.0 or later for Glacier S3 access
3233
33If you install from the source package, you will also need:34If you install from the source package, you will also need:
3435
3536
=== modified file 'bin/duplicity'
--- bin/duplicity 2014-02-05 02:57:01 +0000
+++ bin/duplicity 2014-02-26 19:49:10 +0000
@@ -735,6 +735,15 @@
735 log.Progress(_('Processed volume %d of %d') % (cur_vol[0], num_vols),735 log.Progress(_('Processed volume %d of %d') % (cur_vol[0], num_vols),
736 cur_vol[0], num_vols)736 cur_vol[0], num_vols)
737737
738 if hasattr(globals.backend, 'pre_process_download'):
739 file_names = []
740 for backup_set in backup_setlist:
741 manifest = backup_set.get_manifest()
742 volumes = manifest.get_containing_volumes(index)
743 for vol_num in volumes:
744 file_names.append(backup_set.volume_name_dict[vol_num])
745 globals.backend.pre_process_download(file_names)
746
738 fileobj_iters = map(get_fileobj_iter, backup_setlist)747 fileobj_iters = map(get_fileobj_iter, backup_setlist)
739 tarfiles = map(patchdir.TarFile_FromFileobjs, fileobj_iters)748 tarfiles = map(patchdir.TarFile_FromFileobjs, fileobj_iters)
740 return patchdir.tarfiles2rop_iter(tarfiles, index)749 return patchdir.tarfiles2rop_iter(tarfiles, index)
@@ -1142,6 +1151,8 @@
1142 local_missing = [] # don't download if we can't decrypt1151 local_missing = [] # don't download if we can't decrypt
1143 for fn in local_spurious:1152 for fn in local_spurious:
1144 remove_local(fn)1153 remove_local(fn)
1154 if hasattr(globals.backend, 'pre_process_download'):
1155 globals.backend.pre_process_download(local_missing)
1145 for fn in local_missing:1156 for fn in local_missing:
1146 copy_to_local(fn)1157 copy_to_local(fn)
1147 else:1158 else:
11481159
=== modified file 'bin/duplicity.1'
--- bin/duplicity.1 2014-01-31 12:41:00 +0000
+++ bin/duplicity.1 2014-02-26 19:49:10 +0000
@@ -778,6 +778,44 @@
778characters or other characters that are not valid in a hostname.778characters or other characters that are not valid in a hostname.
779779
780.TP780.TP
781.BI "--s3-use-rrs"
782Store volumes using Reduced Redundnacy Storage when uploading to Amazon S3.
783This will lower the cost of storage but also lower the durability of stored
784volumnes to 99.99% instead the 99.999999999% durability offered by Standard
785Storage on S3.
786
787.TP
788.BI "--s3-use-multiprocessing"
789Allow multipart volumne uploads to S3 through multiprocessing. This option
790requires Python 2.6 and can be used to make uploads to S3 more efficient.
791If enabled, files duplicity uploads to S3 will be split into chunks and
792uploaded in parallel. Useful if you want to saturate your bandwidth
793or if large files are failing during upload.
794
795.TP
796.BI "--s3-multipart-chunk-size"
797Chunk size (in MB) used for S3 multipart uploads. Make this smaller than
798.B --volsize
799to maximize the use of your bandwidth. For example, a chunk size of 10MB
800with a volsize of 30MB will result in 3 chunks per volume upload.
801
802.TP
803.BI "--s3-multipart-max-procs"
804Specify the maximum number of processes to spawn when performing a multipart
805upload to S3. By default, this will choose the number of processors detected
806on your system (e.g. 4 for a 4-core system). You can adjust this number as
807required to ensure you don't overload your system while maximizing the use of
808your bandwidth.
809
810.TP
811.BI "--s3_multipart_max_timeout"
812You can control the maximum time (in seconds) a multipart upload can spend on
813uploading a single chunk to S3. This may be useful if you find your system
814hanging on multipart uploads or if you'd like to control the time variance
815when uploading to S3 to ensure you kill connections to slow S3 endpoints.
816
817
818.TP
781.BI "--scp-command " command819.BI "--scp-command " command
782.B (only ssh pexpect backend with --use-scp enabled)820.B (only ssh pexpect backend with --use-scp enabled)
783The821The
784822
=== modified file 'duplicity/backends/_boto_multi.py'
--- duplicity/backends/_boto_multi.py 2014-01-13 15:54:13 +0000
+++ duplicity/backends/_boto_multi.py 2014-02-26 19:49:10 +0000
@@ -22,20 +22,20 @@
2222
23import os23import os
24import sys24import sys
25import time
26import threading25import threading
27import Queue26import Queue
2827import time
29import duplicity.backend28import traceback
3029
31from duplicity import globals30from duplicity import globals
32from duplicity import log31from duplicity import log
33from duplicity.errors import * #@UnusedWildImport32from duplicity.errors import * #@UnusedWildImport
34from duplicity.util import exception_traceback
35from duplicity.backend import retry
36from duplicity.filechunkio import FileChunkIO33from duplicity.filechunkio import FileChunkIO
37from duplicity import progress34from duplicity import progress
3835
36from _boto_single import BotoBackend as BotoSingleBackend
37from _boto_single import get_connection
38
39BOTO_MIN_VERSION = "2.1.1"39BOTO_MIN_VERSION = "2.1.1"
4040
41# Multiprocessing is not supported on *BSD41# Multiprocessing is not supported on *BSD
@@ -61,100 +61,13 @@
61 def run(self):61 def run(self):
62 while not self.finish:62 while not self.finish:
63 try:63 try:
64 args = self.queue.get(True, 1) 64 args = self.queue.get(True, 1)
65 progress.report_transfer(args[0], args[1])65 progress.report_transfer(args[0], args[1])
66 except Queue.Empty, e:66 except Queue.Empty, e:
67 pass67 pass
68 68
6969
70def get_connection(scheme, parsed_url):70class BotoBackend(BotoSingleBackend):
71 try:
72 import boto
73 assert boto.Version >= BOTO_MIN_VERSION
74
75 from boto.s3.connection import S3Connection
76 assert hasattr(S3Connection, 'lookup')
77
78 # Newer versions of boto default to using
79 # virtual hosting for buckets as a result of
80 # upstream deprecation of the old-style access
81 # method by Amazon S3. This change is not
82 # backwards compatible (in particular with
83 # respect to upper case characters in bucket
84 # names); so we default to forcing use of the
85 # old-style method unless the user has
86 # explicitly asked us to use new-style bucket
87 # access.
88 #
89 # Note that if the user wants to use new-style
90 # buckets, we use the subdomain calling form
91 # rather than given the option of both
92 # subdomain and vhost. The reason being that
93 # anything addressable as a vhost, is also
94 # addressable as a subdomain. Seeing as the
95 # latter is mostly a convenience method of
96 # allowing browse:able content semi-invisibly
97 # being hosted on S3, the former format makes
98 # a lot more sense for us to use - being
99 # explicit about what is happening (the fact
100 # that we are talking to S3 servers).
101
102 try:
103 from boto.s3.connection import OrdinaryCallingFormat
104 from boto.s3.connection import SubdomainCallingFormat
105 cfs_supported = True
106 calling_format = OrdinaryCallingFormat()
107 except ImportError:
108 cfs_supported = False
109 calling_format = None
110
111 if globals.s3_use_new_style:
112 if cfs_supported:
113 calling_format = SubdomainCallingFormat()
114 else:
115 log.FatalError("Use of new-style (subdomain) S3 bucket addressing was"
116 "requested, but does not seem to be supported by the "
117 "boto library. Either you need to upgrade your boto "
118 "library or duplicity has failed to correctly detect "
119 "the appropriate support.",
120 log.ErrorCode.boto_old_style)
121 else:
122 if cfs_supported:
123 calling_format = OrdinaryCallingFormat()
124 else:
125 calling_format = None
126
127 except ImportError:
128 log.FatalError("This backend (s3) requires boto library, version %s or later, "
129 "(http://code.google.com/p/boto/)." % BOTO_MIN_VERSION,
130 log.ErrorCode.boto_lib_too_old)
131
132 if scheme == 's3+http':
133 # Use the default Amazon S3 host.
134 conn = S3Connection(is_secure=(not globals.s3_unencrypted_connection))
135 else:
136 assert scheme == 's3'
137 conn = S3Connection(
138 host = parsed_url.hostname,
139 is_secure=(not globals.s3_unencrypted_connection))
140
141 if hasattr(conn, 'calling_format'):
142 if calling_format is None:
143 log.FatalError("It seems we previously failed to detect support for calling "
144 "formats in the boto library, yet the support is there. This is "
145 "almost certainly a duplicity bug.",
146 log.ErrorCode.boto_calling_format)
147 else:
148 conn.calling_format = calling_format
149
150 else:
151 # Duplicity hangs if boto gets a null bucket name.
152 # HC: Caught a socket error, trying to recover
153 raise BackendException('Boto requires a bucket name.')
154 return conn
155
156
157class BotoBackend(duplicity.backend.Backend):
158 """71 """
159 Backend for Amazon's Simple Storage System, (aka Amazon S3), though72 Backend for Amazon's Simple Storage System, (aka Amazon S3), though
160 the use of the boto module, (http://code.google.com/p/boto/).73 the use of the boto module, (http://code.google.com/p/boto/).
@@ -167,199 +80,32 @@
167 """80 """
16881
169 def __init__(self, parsed_url):82 def __init__(self, parsed_url):
170 duplicity.backend.Backend.__init__(self, parsed_url)83 BotoSingleBackend.__init__(self, parsed_url)
17184 self._setup_pool()
172 from boto.s3.key import Key85
173 from boto.s3.multipart import MultiPartUpload86 def _setup_pool(self):
17487 number_of_procs = globals.s3_multipart_max_procs
175 # This folds the null prefix and all null parts, which means that:88 if not number_of_procs:
176 # //MyBucket/ and //MyBucket are equivalent.89 number_of_procs = multiprocessing.cpu_count()
177 # //MyBucket//My///My/Prefix/ and //MyBucket/My/Prefix are equivalent.90
178 self.url_parts = filter(lambda x: x != '', parsed_url.path.split('/'))91 if getattr(self, '_pool', False):
17992 log.Debug("A process pool already exists. Destroying previous pool.")
180 if self.url_parts:93 self._pool.terminate()
181 self.bucket_name = self.url_parts.pop(0)94 self._pool.join()
182 else:95 self._pool = None
183 # Duplicity hangs if boto gets a null bucket name.96
184 # HC: Caught a socket error, trying to recover97 log.Debug("Setting multipart boto backend process pool to %d processes" % number_of_procs)
185 raise BackendException('Boto requires a bucket name.')98
18699 self._pool = multiprocessing.Pool(processes=number_of_procs)
187 self.scheme = parsed_url.scheme100
188101 def close(self):
189 self.key_class = Key102 BotoSingleBackend.close(self)
190103 log.Debug("Closing pool")
191 if self.url_parts:104 self._pool.terminate()
192 self.key_prefix = '%s/' % '/'.join(self.url_parts)105 self._pool.join()
193 else:
194 self.key_prefix = ''
195
196 self.straight_url = duplicity.backend.strip_auth_from_url(parsed_url)
197 self.parsed_url = parsed_url
198 self.resetConnection()
199
200 def resetConnection(self):
201 self.bucket = None
202 self.conn = get_connection(self.scheme, self.parsed_url)
203 self.bucket = self.conn.lookup(self.bucket_name)
204
205 def put(self, source_path, remote_filename=None):
206 from boto.s3.connection import Location
207 if globals.s3_european_buckets:
208 if not globals.s3_use_new_style:
209 log.FatalError("European bucket creation was requested, but not new-style "
210 "bucket addressing (--s3-use-new-style)",
211 log.ErrorCode.s3_bucket_not_style)
212 #Network glitch may prevent first few attempts of creating/looking up a bucket
213 for n in range(1, globals.num_retries+1):
214 if self.bucket:
215 break
216 if n > 1:
217 time.sleep(30)
218 try:
219 try:
220 self.bucket = self.conn.get_bucket(self.bucket_name, validate=True)
221 except Exception, e:
222 if "NoSuchBucket" in str(e):
223 if globals.s3_european_buckets:
224 self.bucket = self.conn.create_bucket(self.bucket_name,
225 location=Location.EU)
226 else:
227 self.bucket = self.conn.create_bucket(self.bucket_name)
228 else:
229 raise e
230 except Exception, e:
231 log.Warn("Failed to create bucket (attempt #%d) '%s' failed (reason: %s: %s)"
232 "" % (n, self.bucket_name,
233 e.__class__.__name__,
234 str(e)))
235 self.resetConnection()
236
237 if not remote_filename:
238 remote_filename = source_path.get_filename()
239 key = self.key_prefix + remote_filename
240 for n in range(1, globals.num_retries+1):
241 if n > 1:
242 # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long)
243 time.sleep(10)
244
245 if globals.s3_use_rrs:
246 storage_class = 'REDUCED_REDUNDANCY'
247 else:
248 storage_class = 'STANDARD'
249 log.Info("Uploading %s/%s to %s Storage" % (self.straight_url, remote_filename, storage_class))
250 try:
251 headers = {
252 'Content-Type': 'application/octet-stream',
253 'x-amz-storage-class': storage_class
254 }
255 self.upload(source_path.name, key, headers)
256 self.resetConnection()
257 return
258 except Exception, e:
259 log.Warn("Upload '%s/%s' failed (attempt #%d, reason: %s: %s)"
260 "" % (self.straight_url,
261 remote_filename,
262 n,
263 e.__class__.__name__,
264 str(e)))
265 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))
266 self.resetConnection()
267 log.Warn("Giving up trying to upload %s/%s after %d attempts" %
268 (self.straight_url, remote_filename, globals.num_retries))
269 raise BackendException("Error uploading %s/%s" % (self.straight_url, remote_filename))
270
271 def get(self, remote_filename, local_path):
272 key = self.key_class(self.bucket)
273 key.key = self.key_prefix + remote_filename
274 for n in range(1, globals.num_retries+1):
275 if n > 1:
276 # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long)
277 time.sleep(10)
278 log.Info("Downloading %s/%s" % (self.straight_url, remote_filename))
279 try:
280 key.get_contents_to_filename(local_path.name)
281 local_path.setdata()
282 self.resetConnection()
283 return
284 except Exception, e:
285 log.Warn("Download %s/%s failed (attempt #%d, reason: %s: %s)"
286 "" % (self.straight_url,
287 remote_filename,
288 n,
289 e.__class__.__name__,
290 str(e)), 1)
291 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))
292 self.resetConnection()
293 log.Warn("Giving up trying to download %s/%s after %d attempts" %
294 (self.straight_url, remote_filename, globals.num_retries))
295 raise BackendException("Error downloading %s/%s" % (self.straight_url, remote_filename))
296
297 def _list(self):
298 if not self.bucket:
299 raise BackendException("No connection to backend")
300
301 for n in range(1, globals.num_retries+1):
302 if n > 1:
303 # sleep before retry
304 time.sleep(30)
305 log.Info("Listing %s" % self.straight_url)
306 try:
307 return self._list_filenames_in_bucket()
308 except Exception, e:
309 log.Warn("List %s failed (attempt #%d, reason: %s: %s)"
310 "" % (self.straight_url,
311 n,
312 e.__class__.__name__,
313 str(e)), 1)
314 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))
315 log.Warn("Giving up trying to list %s after %d attempts" %
316 (self.straight_url, globals.num_retries))
317 raise BackendException("Error listng %s" % self.straight_url)
318
319 def _list_filenames_in_bucket(self):
320 # We add a 'd' to the prefix to make sure it is not null (for boto) and
321 # to optimize the listing of our filenames, which always begin with 'd'.
322 # This will cause a failure in the regression tests as below:
323 # FAIL: Test basic backend operations
324 # <tracback snipped>
325 # AssertionError: Got list: []
326 # Wanted: ['testfile']
327 # Because of the need for this optimization, it should be left as is.
328 #for k in self.bucket.list(prefix = self.key_prefix + 'd', delimiter = '/'):
329 filename_list = []
330 for k in self.bucket.list(prefix = self.key_prefix, delimiter = '/'):
331 try:
332 filename = k.key.replace(self.key_prefix, '', 1)
333 filename_list.append(filename)
334 log.Debug("Listed %s/%s" % (self.straight_url, filename))
335 except AttributeError:
336 pass
337 return filename_list
338
339 def delete(self, filename_list):
340 for filename in filename_list:
341 self.bucket.delete_key(self.key_prefix + filename)
342 log.Debug("Deleted %s/%s" % (self.straight_url, filename))
343
344 @retry
345 def _query_file_info(self, filename, raise_errors=False):
346 try:
347 key = self.bucket.lookup(self.key_prefix + filename)
348 if key is None:
349 return {'size': -1}
350 return {'size': key.size}
351 except Exception, e:
352 log.Warn("Query %s/%s failed: %s"
353 "" % (self.straight_url,
354 filename,
355 str(e)))
356 self.resetConnection()
357 if raise_errors:
358 raise e
359 else:
360 return {'size': None}
361106
362 def upload(self, filename, key, headers=None):107 def upload(self, filename, key, headers=None):
108 import boto
363 chunk_size = globals.s3_multipart_chunk_size109 chunk_size = globals.s3_multipart_chunk_size
364110
365 # Check minimum chunk size for S3111 # Check minimum chunk size for S3
@@ -379,7 +125,7 @@
379125
380 log.Debug("Uploading %d bytes in %d chunks" % (bytes, chunks))126 log.Debug("Uploading %d bytes in %d chunks" % (bytes, chunks))
381127
382 mp = self.bucket.initiate_multipart_upload(key, headers)128 mp = self.bucket.initiate_multipart_upload(key.key, headers)
383129
384 # Initiate a queue to share progress data between the pool130 # Initiate a queue to share progress data between the pool
385 # workers and a consumer thread, that will collect and report131 # workers and a consumer thread, that will collect and report
@@ -389,57 +135,81 @@
389 queue = manager.Queue()135 queue = manager.Queue()
390 consumer = ConsumerThread(queue)136 consumer = ConsumerThread(queue)
391 consumer.start()137 consumer.start()
392138 tasks = []
393 pool = multiprocessing.Pool(processes=chunks)
394 for n in range(chunks):139 for n in range(chunks):
395 params = [self.scheme, self.parsed_url, self.bucket_name, 140 storage_uri = boto.storage_uri(self.boto_uri_str)
396 mp.id, filename, n, chunk_size, globals.num_retries, 141 params = [self.scheme, self.parsed_url, storage_uri, self.bucket_name,
397 queue]142 mp.id, filename, n, chunk_size, globals.num_retries,
398 pool.apply_async(multipart_upload_worker, params)143 queue]
399 pool.close()144 tasks.append(self._pool.apply_async(multipart_upload_worker, params))
400 pool.join()145
146 log.Debug("Waiting for the pool to finish processing %s tasks" % len(tasks))
147 while tasks:
148 try:
149 tasks[0].wait(timeout=globals.s3_multipart_max_timeout)
150 if tasks[0].ready():
151 if tasks[0].successful():
152 del tasks[0]
153 else:
154 log.Debug("Part upload not successful, aborting multipart upload.")
155 self._setup_pool()
156 break
157 else:
158 raise multiprocessing.TimeoutError
159 except multiprocessing.TimeoutError:
160 log.Debug("%s tasks did not finish by the specified timeout, aborting multipart upload and resetting pool." % len(tasks))
161 self._setup_pool()
162 break
163
164 log.Debug("Done waiting for the pool to finish processing")
401165
402 # Terminate the consumer thread, if any166 # Terminate the consumer thread, if any
403 if globals.progress:167 if globals.progress:
404 consumer.finish = True168 consumer.finish = True
405 consumer.join()169 consumer.join()
406170
407 if len(mp.get_all_parts()) < chunks:171 if len(tasks) > 0 or len(mp.get_all_parts()) < chunks:
408 mp.cancel_upload()172 mp.cancel_upload()
409 raise BackendException("Multipart upload failed. Aborted.")173 raise BackendException("Multipart upload failed. Aborted.")
410174
411 return mp.complete_upload()175 return mp.complete_upload()
412176
413177
414def multipart_upload_worker(scheme, parsed_url, bucket_name, multipart_id, filename,178def multipart_upload_worker(scheme, parsed_url, storage_uri, bucket_name, multipart_id,
415 offset, bytes, num_retries, queue):179 filename, offset, bytes, num_retries, queue):
416 """180 """
417 Worker method for uploading a file chunk to S3 using multipart upload.181 Worker method for uploading a file chunk to S3 using multipart upload.
418 Note that the file chunk is read into memory, so it's important to keep182 Note that the file chunk is read into memory, so it's important to keep
419 this number reasonably small.183 this number reasonably small.
420 """184 """
421 import traceback
422185
423 def _upload_callback(uploaded, total):186 def _upload_callback(uploaded, total):
424 worker_name = multiprocessing.current_process().name187 worker_name = multiprocessing.current_process().name
425 log.Debug("%s: Uploaded %s/%s bytes" % (worker_name, uploaded, total))188 log.Debug("%s: Uploaded %s/%s bytes" % (worker_name, uploaded, total))
426 if not queue is None:189 if not queue is None:
427 queue.put([uploaded, total]) # Push data to the consumer thread190 queue.put([uploaded, total]) # Push data to the consumer thread
428191
429 def _upload(num_retries):192 def _upload(num_retries):
430 worker_name = multiprocessing.current_process().name193 worker_name = multiprocessing.current_process().name
431 log.Debug("%s: Uploading chunk %d" % (worker_name, offset + 1))194 log.Debug("%s: Uploading chunk %d" % (worker_name, offset + 1))
432 try:195 try:
433 conn = get_connection(scheme, parsed_url)196 conn = get_connection(scheme, parsed_url, storage_uri)
434 bucket = conn.lookup(bucket_name)197 bucket = conn.lookup(bucket_name)
435198
436 for mp in bucket.get_all_multipart_uploads():199 for mp in bucket.list_multipart_uploads():
437 if mp.id == multipart_id:200 if mp.id == multipart_id:
438 with FileChunkIO(filename, 'r', offset=offset * bytes, bytes=bytes) as fd:201 with FileChunkIO(filename, 'r', offset=offset * bytes, bytes=bytes) as fd:
202 start = time.time()
439 mp.upload_part_from_file(fd, offset + 1, cb=_upload_callback,203 mp.upload_part_from_file(fd, offset + 1, cb=_upload_callback,
440 num_cb=max(2, 8 * bytes / (1024 * 1024))204 num_cb=max(2, 8 * bytes / (1024 * 1024))
441 ) # Max num of callbacks = 8 times x megabyte205 ) # Max num of callbacks = 8 times x megabyte
206 end = time.time()
207 log.Debug("{name}: Uploaded chunk {chunk} at roughly {speed} bytes/second".format(name=worker_name, chunk=offset+1, speed=(bytes/max(1, abs(end-start)))))
442 break208 break
209 conn.close()
210 conn = None
211 bucket = None
212 del conn
443 except Exception, e:213 except Exception, e:
444 traceback.print_exc()214 traceback.print_exc()
445 if num_retries:215 if num_retries:
@@ -452,6 +222,3 @@
452 log.Debug("%s: Upload of chunk %d complete" % (worker_name, offset + 1))222 log.Debug("%s: Upload of chunk %d complete" % (worker_name, offset + 1))
453223
454 return _upload(num_retries)224 return _upload(num_retries)
455
456duplicity.backend.register_backend("s3", BotoBackend)
457duplicity.backend.register_backend("s3+http", BotoBackend)
458225
=== modified file 'duplicity/backends/_boto_single.py'
--- duplicity/backends/_boto_single.py 2014-01-13 15:54:13 +0000
+++ duplicity/backends/_boto_single.py 2014-02-26 19:49:10 +0000
@@ -19,6 +19,7 @@
19# along with duplicity; if not, write to the Free Software Foundation,19# along with duplicity; if not, write to the Free Software Foundation,
20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2121
22import os
22import time23import time
2324
24import duplicity.backend25import duplicity.backend
@@ -29,7 +30,90 @@
29from duplicity.backend import retry30from duplicity.backend import retry
30from duplicity import progress31from duplicity import progress
3132
32BOTO_MIN_VERSION = "2.0"33BOTO_MIN_VERSION = "2.1.1"
34
35
36def get_connection(scheme, parsed_url, storage_uri):
37 try:
38 from boto.s3.connection import S3Connection
39 assert hasattr(S3Connection, 'lookup')
40
41 # Newer versions of boto default to using
42 # virtual hosting for buckets as a result of
43 # upstream deprecation of the old-style access
44 # method by Amazon S3. This change is not
45 # backwards compatible (in particular with
46 # respect to upper case characters in bucket
47 # names); so we default to forcing use of the
48 # old-style method unless the user has
49 # explicitly asked us to use new-style bucket
50 # access.
51 #
52 # Note that if the user wants to use new-style
53 # buckets, we use the subdomain calling form
54 # rather than given the option of both
55 # subdomain and vhost. The reason being that
56 # anything addressable as a vhost, is also
57 # addressable as a subdomain. Seeing as the
58 # latter is mostly a convenience method of
59 # allowing browse:able content semi-invisibly
60 # being hosted on S3, the former format makes
61 # a lot more sense for us to use - being
62 # explicit about what is happening (the fact
63 # that we are talking to S3 servers).
64
65 try:
66 from boto.s3.connection import OrdinaryCallingFormat
67 from boto.s3.connection import SubdomainCallingFormat
68 cfs_supported = True
69 calling_format = OrdinaryCallingFormat()
70 except ImportError:
71 cfs_supported = False
72 calling_format = None
73
74 if globals.s3_use_new_style:
75 if cfs_supported:
76 calling_format = SubdomainCallingFormat()
77 else:
78 log.FatalError("Use of new-style (subdomain) S3 bucket addressing was"
79 "requested, but does not seem to be supported by the "
80 "boto library. Either you need to upgrade your boto "
81 "library or duplicity has failed to correctly detect "
82 "the appropriate support.",
83 log.ErrorCode.boto_old_style)
84 else:
85 if cfs_supported:
86 calling_format = OrdinaryCallingFormat()
87 else:
88 calling_format = None
89
90 except ImportError:
91 log.FatalError("This backend (s3) requires boto library, version %s or later, "
92 "(http://code.google.com/p/boto/)." % BOTO_MIN_VERSION,
93 log.ErrorCode.boto_lib_too_old)
94
95 if not parsed_url.hostname:
96 # Use the default host.
97 conn = storage_uri.connect(is_secure=(not globals.s3_unencrypted_connection))
98 else:
99 assert scheme == 's3'
100 conn = storage_uri.connect(host=parsed_url.hostname,
101 is_secure=(not globals.s3_unencrypted_connection))
102
103 if hasattr(conn, 'calling_format'):
104 if calling_format is None:
105 log.FatalError("It seems we previously failed to detect support for calling "
106 "formats in the boto library, yet the support is there. This is "
107 "almost certainly a duplicity bug.",
108 log.ErrorCode.boto_calling_format)
109 else:
110 conn.calling_format = calling_format
111
112 else:
113 # Duplicity hangs if boto gets a null bucket name.
114 # HC: Caught a socket error, trying to recover
115 raise BackendException('Boto requires a bucket name.')
116 return conn
33117
34118
35class BotoBackend(duplicity.backend.Backend):119class BotoBackend(duplicity.backend.Backend):
@@ -76,96 +160,28 @@
76 # boto uses scheme://bucket[/name] and specifies hostname on connect()160 # boto uses scheme://bucket[/name] and specifies hostname on connect()
77 self.boto_uri_str = '://'.join((parsed_url.scheme[:2],161 self.boto_uri_str = '://'.join((parsed_url.scheme[:2],
78 parsed_url.path.lstrip('/')))162 parsed_url.path.lstrip('/')))
79 self.storage_uri = boto.storage_uri(self.boto_uri_str)
80 self.resetConnection()163 self.resetConnection()
164 self._listed_keys = {}
165
166 def close(self):
167 del self._listed_keys
168 self._listed_keys = {}
169 self.bucket = None
170 self.conn = None
171 self.storage_uri = None
172 del self.conn
173 del self.storage_uri
81174
82 def resetConnection(self):175 def resetConnection(self):
176 if getattr(self, 'conn', False):
177 self.conn.close()
83 self.bucket = None178 self.bucket = None
84 self.conn = None179 self.conn = None
85180 self.storage_uri = None
86 try:181 del self.conn
87 from boto.s3.connection import S3Connection182 del self.storage_uri
88 from boto.s3.key import Key183 self.storage_uri = boto.storage_uri(self.boto_uri_str)
89 assert hasattr(S3Connection, 'lookup')184 self.conn = get_connection(self.scheme, self.parsed_url, self.storage_uri)
90
91 # Newer versions of boto default to using
92 # virtual hosting for buckets as a result of
93 # upstream deprecation of the old-style access
94 # method by Amazon S3. This change is not
95 # backwards compatible (in particular with
96 # respect to upper case characters in bucket
97 # names); so we default to forcing use of the
98 # old-style method unless the user has
99 # explicitly asked us to use new-style bucket
100 # access.
101 #
102 # Note that if the user wants to use new-style
103 # buckets, we use the subdomain calling form
104 # rather than given the option of both
105 # subdomain and vhost. The reason being that
106 # anything addressable as a vhost, is also
107 # addressable as a subdomain. Seeing as the
108 # latter is mostly a convenience method of
109 # allowing browse:able content semi-invisibly
110 # being hosted on S3, the former format makes
111 # a lot more sense for us to use - being
112 # explicit about what is happening (the fact
113 # that we are talking to S3 servers).
114
115 try:
116 from boto.s3.connection import OrdinaryCallingFormat
117 from boto.s3.connection import SubdomainCallingFormat
118 cfs_supported = True
119 calling_format = OrdinaryCallingFormat()
120 except ImportError:
121 cfs_supported = False
122 calling_format = None
123
124 if globals.s3_use_new_style:
125 if cfs_supported:
126 calling_format = SubdomainCallingFormat()
127 else:
128 log.FatalError("Use of new-style (subdomain) S3 bucket addressing was"
129 "requested, but does not seem to be supported by the "
130 "boto library. Either you need to upgrade your boto "
131 "library or duplicity has failed to correctly detect "
132 "the appropriate support.",
133 log.ErrorCode.boto_old_style)
134 else:
135 if cfs_supported:
136 calling_format = OrdinaryCallingFormat()
137 else:
138 calling_format = None
139
140 except ImportError:
141 log.FatalError("This backend (s3) requires boto library, version %s or later, "
142 "(http://code.google.com/p/boto/)." % BOTO_MIN_VERSION,
143 log.ErrorCode.boto_lib_too_old)
144
145 if not self.parsed_url.hostname:
146 # Use the default host.
147 self.conn = self.storage_uri.connect(
148 is_secure=(not globals.s3_unencrypted_connection))
149 else:
150 assert self.scheme == 's3'
151 self.conn = self.storage_uri.connect(
152 host=self.parsed_url.hostname,
153 is_secure=(not globals.s3_unencrypted_connection))
154
155 if hasattr(self.conn, 'calling_format'):
156 if calling_format is None:
157 log.FatalError("It seems we previously failed to detect support for calling "
158 "formats in the boto library, yet the support is there. This is "
159 "almost certainly a duplicity bug.",
160 log.ErrorCode.boto_calling_format)
161 else:
162 self.conn.calling_format = calling_format
163
164 else:
165 # Duplicity hangs if boto gets a null bucket name.
166 # HC: Caught a socket error, trying to recover
167 raise BackendException('Boto requires a bucket name.')
168
169 self.bucket = self.conn.lookup(self.bucket_name)185 self.bucket = self.conn.lookup(self.bucket_name)
170186
171 def put(self, source_path, remote_filename=None):187 def put(self, source_path, remote_filename=None):
@@ -181,6 +197,7 @@
181 break197 break
182 if n > 1:198 if n > 1:
183 time.sleep(30)199 time.sleep(30)
200 self.resetConnection()
184 try:201 try:
185 try:202 try:
186 self.bucket = self.conn.get_bucket(self.bucket_name, validate=True)203 self.bucket = self.conn.get_bucket(self.bucket_name, validate=True)
@@ -198,7 +215,6 @@
198 "" % (n, self.bucket_name,215 "" % (n, self.bucket_name,
199 e.__class__.__name__,216 e.__class__.__name__,
200 str(e)))217 str(e)))
201 self.resetConnection()
202218
203 if not remote_filename:219 if not remote_filename:
204 remote_filename = source_path.get_filename()220 remote_filename = source_path.get_filename()
@@ -215,14 +231,17 @@
215 storage_class = 'STANDARD'231 storage_class = 'STANDARD'
216 log.Info("Uploading %s/%s to %s Storage" % (self.straight_url, remote_filename, storage_class))232 log.Info("Uploading %s/%s to %s Storage" % (self.straight_url, remote_filename, storage_class))
217 try:233 try:
218 key.set_contents_from_filename(source_path.name, {'Content-Type': 'application/octet-stream',234 headers = {
219 'x-amz-storage-class': storage_class},235 'Content-Type': 'application/octet-stream',
220 cb=progress.report_transfer,236 'x-amz-storage-class': storage_class
221 num_cb=(max(2, 8 * globals.volsize / (1024 * 1024)))237 }
222 ) # Max num of callbacks = 8 times x megabyte238 upload_start = time.time()
223239 self.upload(source_path.name, key, headers)
224 key.close()240 upload_end = time.time()
241 total_s = abs(upload_end-upload_start) or 1 # prevent a zero value!
242 rough_upload_speed = os.path.getsize(source_path.name)/total_s
225 self.resetConnection()243 self.resetConnection()
244 log.Debug("Uploaded %s/%s to %s Storage at roughly %f bytes/second" % (self.straight_url, remote_filename, storage_class, rough_upload_speed))
226 return245 return
227 except Exception, e:246 except Exception, e:
228 log.Warn("Upload '%s/%s' failed (attempt #%d, reason: %s: %s)"247 log.Warn("Upload '%s/%s' failed (attempt #%d, reason: %s: %s)"
@@ -238,19 +257,18 @@
238 raise BackendException("Error uploading %s/%s" % (self.straight_url, remote_filename))257 raise BackendException("Error uploading %s/%s" % (self.straight_url, remote_filename))
239258
240 def get(self, remote_filename, local_path):259 def get(self, remote_filename, local_path):
260 key_name = self.key_prefix + remote_filename
261 self.pre_process_download(remote_filename, wait=True)
262 key = self._listed_keys[key_name]
241 for n in range(1, globals.num_retries+1):263 for n in range(1, globals.num_retries+1):
242 if n > 1:264 if n > 1:
243 # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long)265 # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long)
244 time.sleep(10)266 time.sleep(10)
245 log.Info("Downloading %s/%s" % (self.straight_url, remote_filename))267 log.Info("Downloading %s/%s" % (self.straight_url, remote_filename))
246 try:268 try:
247 key_name = self.key_prefix + remote_filename269 self.resetConnection()
248 key = self.bucket.get_key(key_name)
249 if key is None:
250 raise BackendException("%s: key not found" % key_name)
251 key.get_contents_to_filename(local_path.name)270 key.get_contents_to_filename(local_path.name)
252 local_path.setdata()271 local_path.setdata()
253 self.resetConnection()
254 return272 return
255 except Exception, e:273 except Exception, e:
256 log.Warn("Download %s/%s failed (attempt #%d, reason: %s: %s)"274 log.Warn("Download %s/%s failed (attempt #%d, reason: %s: %s)"
@@ -260,7 +278,7 @@
260 e.__class__.__name__,278 e.__class__.__name__,
261 str(e)), 1)279 str(e)), 1)
262 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))280 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))
263 self.resetConnection()281
264 log.Warn("Giving up trying to download %s/%s after %d attempts" %282 log.Warn("Giving up trying to download %s/%s after %d attempts" %
265 (self.straight_url, remote_filename, globals.num_retries))283 (self.straight_url, remote_filename, globals.num_retries))
266 raise BackendException("Error downloading %s/%s" % (self.straight_url, remote_filename))284 raise BackendException("Error downloading %s/%s" % (self.straight_url, remote_filename))
@@ -273,6 +291,7 @@
273 if n > 1:291 if n > 1:
274 # sleep before retry292 # sleep before retry
275 time.sleep(30)293 time.sleep(30)
294 self.resetConnection()
276 log.Info("Listing %s" % self.straight_url)295 log.Info("Listing %s" % self.straight_url)
277 try:296 try:
278 return self._list_filenames_in_bucket()297 return self._list_filenames_in_bucket()
@@ -298,10 +317,11 @@
298 # Because of the need for this optimization, it should be left as is.317 # Because of the need for this optimization, it should be left as is.
299 #for k in self.bucket.list(prefix = self.key_prefix + 'd', delimiter = '/'):318 #for k in self.bucket.list(prefix = self.key_prefix + 'd', delimiter = '/'):
300 filename_list = []319 filename_list = []
301 for k in self.bucket.list(prefix = self.key_prefix, delimiter = '/'):320 for k in self.bucket.list(prefix=self.key_prefix, delimiter='/'):
302 try:321 try:
303 filename = k.key.replace(self.key_prefix, '', 1)322 filename = k.key.replace(self.key_prefix, '', 1)
304 filename_list.append(filename)323 filename_list.append(filename)
324 self._listed_keys[k.key] = k
305 log.Debug("Listed %s/%s" % (self.straight_url, filename))325 log.Debug("Listed %s/%s" % (self.straight_url, filename))
306 except AttributeError:326 except AttributeError:
307 pass327 pass
@@ -330,6 +350,53 @@
330 else:350 else:
331 return {'size': None}351 return {'size': None}
332352
333duplicity.backend.register_backend("gs", BotoBackend)353 def upload(self, filename, key, headers):
334duplicity.backend.register_backend("s3", BotoBackend)354 key.set_contents_from_filename(filename, headers,
335duplicity.backend.register_backend("s3+http", BotoBackend)355 cb=progress.report_transfer,
356 num_cb=(max(2, 8 * globals.volsize / (1024 * 1024)))
357 ) # Max num of callbacks = 8 times x megabyte
358 key.close()
359
360 def pre_process_download(self, files_to_download, wait=False):
361 # Used primarily to move files in Glacier to S3
362 if isinstance(files_to_download, basestring):
363 files_to_download = [files_to_download]
364
365 for remote_filename in files_to_download:
366 success = False
367 for n in range(1, globals.num_retries+1):
368 if n > 1:
369 # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long)
370 time.sleep(10)
371 self.resetConnection()
372 try:
373 key_name = self.key_prefix + remote_filename
374 if not self._listed_keys.get(key_name, False):
375 self._listed_keys[key_name] = list(self.bucket.list(key_name))[0]
376 key = self._listed_keys[key_name]
377
378 if key.storage_class == "GLACIER":
379 # We need to move the file out of glacier
380 if not self.bucket.get_key(key.key).ongoing_restore:
381 log.Info("File %s is in Glacier storage, restoring to S3" % remote_filename)
382 key.restore(days=1) # Shouldn't need this again after 1 day
383 if wait:
384 log.Info("Waiting for file %s to restore from Glacier" % remote_filename)
385 while self.bucket.get_key(key.key).ongoing_restore:
386 time.sleep(60)
387 self.resetConnection()
388 log.Info("File %s was successfully restored from Glacier" % remote_filename)
389 success = True
390 break
391 except Exception, e:
392 log.Warn("Restoration from Glacier for file %s/%s failed (attempt #%d, reason: %s: %s)"
393 "" % (self.straight_url,
394 remote_filename,
395 n,
396 e.__class__.__name__,
397 str(e)), 1)
398 log.Debug("Backtrace of previous error: %s" % (exception_traceback(),))
399 if not success:
400 log.Warn("Giving up trying to restore %s/%s after %d attempts" %
401 (self.straight_url, remote_filename, globals.num_retries))
402 raise BackendException("Error restoring %s/%s from Glacier to S3" % (self.straight_url, remote_filename))
336403
=== modified file 'duplicity/backends/botobackend.py'
--- duplicity/backends/botobackend.py 2012-02-29 16:40:41 +0000
+++ duplicity/backends/botobackend.py 2014-02-26 19:49:10 +0000
@@ -20,13 +20,20 @@
20# along with duplicity; if not, write to the Free Software Foundation,20# along with duplicity; if not, write to the Free Software Foundation,
21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2222
23import duplicity.backend
23from duplicity import globals24from duplicity import globals
24import sys25import sys
26from _boto_multi import BotoBackend as BotoMultiUploadBackend
27from _boto_single import BotoBackend as BotoSingleUploadBackend
2528
26if globals.s3_use_multiprocessing:29if globals.s3_use_multiprocessing:
27 if sys.version_info[:2] < (2,6):30 if sys.version_info[:2] < (2, 6):
28 print "Sorry, S3 multiprocessing requires version 2.5 or later of python"31 print "Sorry, S3 multiprocessing requires version 2.6 or later of python"
29 sys.exit(1)32 sys.exit(1)
30 import _boto_multi33 duplicity.backend.register_backend("gs", BotoMultiUploadBackend)
34 duplicity.backend.register_backend("s3", BotoMultiUploadBackend)
35 duplicity.backend.register_backend("s3+http", BotoMultiUploadBackend)
31else:36else:
32 import _boto_single37 duplicity.backend.register_backend("gs", BotoSingleUploadBackend)
38 duplicity.backend.register_backend("s3", BotoSingleUploadBackend)
39 duplicity.backend.register_backend("s3+http", BotoSingleUploadBackend)
3340
=== modified file 'duplicity/commandline.py'
--- duplicity/commandline.py 2014-01-31 12:41:00 +0000
+++ duplicity/commandline.py 2014-02-26 19:49:10 +0000
@@ -495,6 +495,14 @@
495 parser.add_option("--s3-multipart-chunk-size", type = "int", action = "callback", metavar = _("number"),495 parser.add_option("--s3-multipart-chunk-size", type = "int", action = "callback", metavar = _("number"),
496 callback = lambda o, s, v, p: setattr(p.values, "s3_multipart_chunk_size", v * 1024 * 1024))496 callback = lambda o, s, v, p: setattr(p.values, "s3_multipart_chunk_size", v * 1024 * 1024))
497497
498 # Number of processes to set the Processor Pool to when uploading multipart
499 # uploads to S3. Use this to control the maximum simultaneous uploads to S3.
500 parser.add_option("--s3-multipart-max-procs", type="int", metavar=_("number"))
501
502 # Number of seconds to wait for each part of a multipart upload to S3. Use this
503 # to prevent hangups when doing a multipart upload to S3.
504 parser.add_option("--s3_multipart_max_timeout", type="int", metavar=_("number"))
505
498 # Option to allow the s3/boto backend use the multiprocessing version.506 # Option to allow the s3/boto backend use the multiprocessing version.
499 # By default it is off since it does not work for Python 2.4 or 2.5.507 # By default it is off since it does not work for Python 2.4 or 2.5.
500 if sys.version_info[:2] >= (2, 6):508 if sys.version_info[:2] >= (2, 6):
501509
=== modified file 'duplicity/globals.py'
--- duplicity/globals.py 2014-01-31 12:41:00 +0000
+++ duplicity/globals.py 2014-02-26 19:49:10 +0000
@@ -200,6 +200,12 @@
200# Minimum chunk size accepted by S3200# Minimum chunk size accepted by S3
201s3_multipart_minimum_chunk_size = 5 * 1024 * 1024201s3_multipart_minimum_chunk_size = 5 * 1024 * 1024
202202
203# Maximum number of processes to use while doing a multipart upload to S3
204s3_multipart_max_procs = None
205
206# Maximum time to wait for a part to finish when doig a multipart upload to S3
207s3_multipart_max_timeout = None
208
203# Whether to use the full email address as the user name when209# Whether to use the full email address as the user name when
204# logging into an imap server. If false just the user name210# logging into an imap server. If false just the user name
205# part of the email address is used.211# part of the email address is used.

Subscribers

People subscribed via source and target branches

to all changes: