diff --git a/.gitignore b/.gitignore index 4149ff6..4779c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +.DS_Store +duplicity-* +duplicity.spec duplicity/_librsync.so testing/config.py testing/*.log +po/duplicity.pot-e Releases patch *.pyc diff --git a/dist/setup.py b/dist/setup.py index 5ebbb22..01f0032 100755 --- a/dist/setup.py +++ b/dist/setup.py @@ -72,6 +72,7 @@ setup(name="duplicity", maintainer="Kenneth Loafman ", maintainer_email="kenneth@loafman.com", url="http://duplicity.nongnu.org/index.html", + install_requires = ['filechunkio'], packages = ['duplicity', 'duplicity.backends',], package_dir = {"duplicity" : "src", diff --git a/duplicity/backends/botobackend.py b/duplicity/backends/botobackend.py index d57363c..aadd6f6 100644 --- a/duplicity/backends/botobackend.py +++ b/duplicity/backends/botobackend.py @@ -19,7 +19,11 @@ # along with duplicity; if not, write to the Free Software Foundation, # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import os import time +import multiprocessing + +from filechunkio import FileChunkIO import duplicity.backend from duplicity import globals @@ -28,6 +32,90 @@ from duplicity.errors import * #@UnusedWildImport from duplicity.util import exception_traceback from duplicity.backend import retry + +def get_connection(scheme, url): + try: + from boto.s3.connection import S3Connection + assert hasattr(S3Connection, 'lookup') + + # Newer versions of boto default to using + # virtual hosting for buckets as a result of + # upstream deprecation of the old-style access + # method by Amazon S3. This change is not + # backwards compatible (in particular with + # respect to upper case characters in bucket + # names); so we default to forcing use of the + # old-style method unless the user has + # explicitly asked us to use new-style bucket + # access. + # + # Note that if the user wants to use new-style + # buckets, we use the subdomain calling form + # rather than given the option of both + # subdomain and vhost. The reason being that + # anything addressable as a vhost, is also + # addressable as a subdomain. Seeing as the + # latter is mostly a convenience method of + # allowing browse:able content semi-invisibly + # being hosted on S3, the former format makes + # a lot more sense for us to use - being + # explicit about what is happening (the fact + # that we are talking to S3 servers). + + try: + from boto.s3.connection import OrdinaryCallingFormat + from boto.s3.connection import SubdomainCallingFormat + cfs_supported = True + calling_format = OrdinaryCallingFormat() + except ImportError: + cfs_supported = False + calling_format = None + + if globals.s3_use_new_style: + if cfs_supported: + calling_format = SubdomainCallingFormat() + else: + log.FatalError("Use of new-style (subdomain) S3 bucket addressing was" + "requested, but does not seem to be supported by the " + "boto library. Either you need to upgrade your boto " + "library or duplicity has failed to correctly detect " + "the appropriate support.", + log.ErrorCode.boto_old_style) + else: + if cfs_supported: + calling_format = OrdinaryCallingFormat() + else: + calling_format = None + + except ImportError: + log.FatalError("This backend (s3) requires boto library, version 0.9d or later, " + "(http://code.google.com/p/boto/).", + log.ErrorCode.boto_lib_too_old) + if scheme == 's3+http': + # Use the default Amazon S3 host. + conn = S3Connection(is_secure=(not globals.s3_unencrypted_connection)) + else: + assert self.scheme == 's3' + conn = S3Connection( + host=parsed_url.hostname, + is_secure=(not globals.s3_unencrypted_connection)) + + if hasattr(conn, 'calling_format'): + if calling_format is None: + log.FatalError("It seems we previously failed to detect support for calling " + "formats in the boto library, yet the support is there. This is " + "almost certainly a duplicity bug.", + log.ErrorCode.boto_calling_format) + else: + conn.calling_format = calling_format + + else: + # Duplicity hangs if boto gets a null bucket name. + # HC: Caught a socket error, trying to recover + raise BackendException('Boto requires a bucket name.') + return conn + + class BotoBackend(duplicity.backend.Backend): """ Backend for Amazon's Simple Storage System, (aka Amazon S3), though @@ -44,7 +132,8 @@ class BotoBackend(duplicity.backend.Backend): duplicity.backend.Backend.__init__(self, parsed_url) from boto.s3.key import Key - + from boto.s3.multipart import MultiPartUpload + # This folds the null prefix and all null parts, which means that: # //MyBucket/ and //MyBucket are equivalent. # //MyBucket//My///My/Prefix/ and //MyBucket/My/Prefix are equivalent. @@ -72,89 +161,7 @@ class BotoBackend(duplicity.backend.Backend): def resetConnection(self): self.bucket = None - self.conn = None - - try: - from boto.s3.connection import S3Connection - from boto.s3.key import Key - assert hasattr(S3Connection, 'lookup') - - # Newer versions of boto default to using - # virtual hosting for buckets as a result of - # upstream deprecation of the old-style access - # method by Amazon S3. This change is not - # backwards compatible (in particular with - # respect to upper case characters in bucket - # names); so we default to forcing use of the - # old-style method unless the user has - # explicitly asked us to use new-style bucket - # access. - # - # Note that if the user wants to use new-style - # buckets, we use the subdomain calling form - # rather than given the option of both - # subdomain and vhost. The reason being that - # anything addressable as a vhost, is also - # addressable as a subdomain. Seeing as the - # latter is mostly a convenience method of - # allowing browse:able content semi-invisibly - # being hosted on S3, the former format makes - # a lot more sense for us to use - being - # explicit about what is happening (the fact - # that we are talking to S3 servers). - - try: - from boto.s3.connection import OrdinaryCallingFormat - from boto.s3.connection import SubdomainCallingFormat - cfs_supported = True - calling_format = OrdinaryCallingFormat() - except ImportError: - cfs_supported = False - calling_format = None - - if globals.s3_use_new_style: - if cfs_supported: - calling_format = SubdomainCallingFormat() - else: - log.FatalError("Use of new-style (subdomain) S3 bucket addressing was" - "requested, but does not seem to be supported by the " - "boto library. Either you need to upgrade your boto " - "library or duplicity has failed to correctly detect " - "the appropriate support.", - log.ErrorCode.boto_old_style) - else: - if cfs_supported: - calling_format = OrdinaryCallingFormat() - else: - calling_format = None - - except ImportError: - log.FatalError("This backend (s3) requires boto library, version 0.9d or later, " - "(http://code.google.com/p/boto/).", - log.ErrorCode.boto_lib_too_old) - if self.scheme == 's3+http': - # Use the default Amazon S3 host. - self.conn = S3Connection(is_secure=(not globals.s3_unencrypted_connection)) - else: - assert self.scheme == 's3' - self.conn = S3Connection( - host=self.parsed_url.hostname, - is_secure=(not globals.s3_unencrypted_connection)) - - if hasattr(self.conn, 'calling_format'): - if calling_format is None: - log.FatalError("It seems we previously failed to detect support for calling " - "formats in the boto library, yet the support is there. This is " - "almost certainly a duplicity bug.", - log.ErrorCode.boto_calling_format) - else: - self.conn.calling_format = calling_format - - else: - # Duplicity hangs if boto gets a null bucket name. - # HC: Caught a socket error, trying to recover - raise BackendException('Boto requires a bucket name.') - + self.conn = get_connection(self.scheme, self.parsed_url) self.bucket = self.conn.lookup(self.bucket_name) def put(self, source_path, remote_filename=None): @@ -191,8 +198,7 @@ class BotoBackend(duplicity.backend.Backend): if not remote_filename: remote_filename = source_path.get_filename() - key = self.key_class(self.bucket) - key.key = self.key_prefix + remote_filename + key = self.key_prefix + remote_filename for n in range(1, globals.num_retries+1): if n > 1: # sleep before retry (new connection to a **hopeful** new host, so no need to wait so long) @@ -204,9 +210,11 @@ class BotoBackend(duplicity.backend.Backend): storage_class = 'STANDARD' log.Info("Uploading %s/%s to %s Storage" % (self.straight_url, remote_filename, storage_class)) try: - key.set_contents_from_filename(source_path.name, {'Content-Type': 'application/octet-stream', - 'x-amz-storage-class': storage_class}) - key.close() + headers = { + 'Content-Type': 'application/octet-stream', + 'x-amz-storage-class': storage_class + } + self.upload(source_path.name, key, headers) self.resetConnection() return except Exception, e: @@ -313,6 +321,88 @@ class BotoBackend(duplicity.backend.Backend): else: return {'size': None} + def upload(self, filename, key, headers=None): + chunk_size = globals.s3_multipart_chunk_size + + # Check minimum chunk size for S3 + if chunk_size < globals.s3_multipart_minimum_chunk_size: + log.Warn("Minimum chunk size is %d, but %d specified." % ( + globals.s3_multipart_minimum_chunk_size, chunk_size)) + chunk_size = globals.s3_multipart_minimum_chunk_size + + # Decide in how many chunks to upload + bytes = os.path.getsize(filename) + if bytes < chunk_size: + chunks = 1 + else: + chunks = bytes / chunk_size + if (bytes % chunk_size): + chunks += 1 + + log.Debug("Uploading %d bytes in %d chunks" % (bytes, chunks)) + + mp = self.bucket.initiate_multipart_upload(key, headers) + + pool = multiprocessing.Pool(processes=chunks) + for n in range(chunks): + params = { + 'scheme': self.scheme, + 'url': self.parsed_url, + 'bucket_name': self.bucket_name, + 'multipart_id': mp.id, + 'filename': filename, + 'offset': n, + 'bytes': chunk_size, + 'num_retries': globals.num_retries, + } + pool.apply_async(multipart_upload_worker, kwds=params) + pool.close() + pool.join() + + if len(mp.get_all_parts()) < chunks: + mp.cancel_upload() + raise BackendException("Multipart upload failed. Aborted.") + + return mp.complete_upload() + + +def multipart_upload_worker(scheme, url, bucket_name, multipart_id, filename, + offset, bytes, num_retries): + """ + Worker method for uploading a file chunk to S3 using multipart upload. + Note that the file chunk is read into memory, so it's important to keep + this number reasonably small. + """ + import traceback + + def _upload_callback(uploaded, total): + worker_name = multiprocessing.current_process().name + log.Debug("%s: Uploaded %s/%s bytes" % (worker_name, uploaded, total)) + + def _upload(num_retries): + worker_name = multiprocessing.current_process().name + log.Debug("%s: Uploading chunk %d" % (worker_name, offset + 1)) + try: + conn = get_connection(scheme, url) + bucket = conn.lookup(bucket_name) + + for mp in bucket.get_all_multipart_uploads(): + if mp.id == multipart_id: + with FileChunkIO(filename, 'r', offset=offset, bytes=bytes) as fd: + mp.upload_part_from_file(fd, offset + 1, cb=_upload_callback) + break + except Exception, e: + traceback.print_exc() + if num_retries: + log.Debug("%s: Upload of chunk %d failed. Retrying %d more times..." % ( + worker_name, offset + 1, num_retries - 1)) + return _upload(num_retries - 1) + log.Debug("%s: Upload of chunk %d failed. Aborting..." % ( + worker_name, offset + 1)) + raise e + log.Debug("%s: Upload of chunk %d complete" % (worker_name, offset + 1)) + + return _upload(num_retries) + duplicity.backend.register_backend("s3", BotoBackend) duplicity.backend.register_backend("s3+http", BotoBackend) - diff --git a/duplicity/commandline.py b/duplicity/commandline.py index 1082c05..c4aa762 100644 --- a/duplicity/commandline.py +++ b/duplicity/commandline.py @@ -421,6 +421,12 @@ def parse_cmdline_options(arglist): # See . parser.add_option("--s3-unencrypted-connection", action="store_true") + # Chunk size used for S3 multipart uploads.The number of parallel uploads to + # S3 be given by chunk size / volume size. Use this to maximize the use of + # your bandwidth. Defaults to 25MB + parser.add_option("--s3-multipart-chunk-size", type="int", action="callback", metavar=_("number"), + callback=lambda o, s, v, p: setattr(p.values, "s3_multipart_chunk_size", v*1024*1024)) + # scp command to use # TRANSL: noun parser.add_option("--scp-command", metavar=_("command")) diff --git a/duplicity/globals.py b/duplicity/globals.py index 6532d78..03887e6 100644 --- a/duplicity/globals.py +++ b/duplicity/globals.py @@ -166,6 +166,14 @@ s3_unencrypted_connection = False # Whether to use S3 Reduced Redudancy Storage s3_use_rrs = False +# Chunk size used for S3 multipart uploads.The number of parallel uploads to +# S3 be given by chunk size / volume size. Use this to maximize the use of +# your bandwidth. Defaults to 25MB +s3_multipart_chunk_size = 25*1024*1024 + +# Minimum chunk size accepted by S3 +s3_multipart_minimum_chunk_size = 5*1024*1024 + # Whether to use the full email address as the user name when # logging into an imap server. If false just the user name # part of the email address is used. diff --git a/po/duplicity.pot b/po/duplicity.pot index caced98..45a3d1f 100644 --- a/po/duplicity.pot +++ b/po/duplicity.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: Kenneth Loafman \n" -"POT-Creation-Date: 2011-08-19 13:26-0500\n" +"POT-Creation-Date: 2011-09-22 20:12-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,11 +18,11 @@ msgstr "" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #: ../duplicity-bin:94 -msgid "Reuse already set PASSPHRASE as SIGNING_PASSPHRASE" +msgid "Reuse configured PASSPHRASE as SIGN_PASSPHRASE" msgstr "" #: ../duplicity-bin:100 -msgid "Reuse already set SIGNING_PASSPHRASE as PASSPHRASE" +msgid "Reuse configured SIGN_PASSPHRASE as PASSPHRASE" msgstr "" #: ../duplicity-bin:139 @@ -68,202 +68,207 @@ msgid "" "Continuing restart on file %s." msgstr "" -#: ../duplicity-bin:481 +#: ../duplicity-bin:281 +#, python-format +msgid "File %s was corrupted during upload." +msgstr "" + +#: ../duplicity-bin:495 msgid "" "Fatal Error: Unable to start incremental backup. Old signatures not found " "and incremental specified" msgstr "" -#: ../duplicity-bin:485 +#: ../duplicity-bin:499 msgid "No signatures found, switching to full backup." msgstr "" -#: ../duplicity-bin:499 +#: ../duplicity-bin:513 msgid "Backup Statistics" msgstr "" -#: ../duplicity-bin:579 +#: ../duplicity-bin:593 #, python-format msgid "%s not found in archive, no files restored." msgstr "" -#: ../duplicity-bin:583 +#: ../duplicity-bin:597 msgid "No files found in archive - nothing restored." msgstr "" -#: ../duplicity-bin:616 +#: ../duplicity-bin:630 #, python-format msgid "Processed volume %d of %d" msgstr "" -#: ../duplicity-bin:641 +#: ../duplicity-bin:655 #, python-format msgid "Invalid data - %s hash mismatch for file:" msgstr "" -#: ../duplicity-bin:643 +#: ../duplicity-bin:657 #, python-format msgid "Calculated hash: %s" msgstr "" -#: ../duplicity-bin:644 +#: ../duplicity-bin:658 #, python-format msgid "Manifest hash: %s" msgstr "" -#: ../duplicity-bin:682 +#: ../duplicity-bin:696 #, python-format msgid "Volume was signed by key %s, not %s" msgstr "" -#: ../duplicity-bin:712 +#: ../duplicity-bin:726 #, python-format msgid "Verify complete: %s, %s." msgstr "" -#: ../duplicity-bin:713 +#: ../duplicity-bin:727 #, python-format msgid "%d file compared" msgid_plural "%d files compared" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:715 +#: ../duplicity-bin:729 #, python-format msgid "%d difference found" msgid_plural "%d differences found" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:734 +#: ../duplicity-bin:748 msgid "No extraneous files found, nothing deleted in cleanup." msgstr "" -#: ../duplicity-bin:739 +#: ../duplicity-bin:753 msgid "Deleting this file from backend:" msgid_plural "Deleting these files from backend:" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:751 +#: ../duplicity-bin:765 msgid "Found the following file to delete:" msgid_plural "Found the following files to delete:" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:755 +#: ../duplicity-bin:769 msgid "Run duplicity again with the --force option to actually delete." msgstr "" -#: ../duplicity-bin:794 +#: ../duplicity-bin:808 msgid "There are backup set(s) at time(s):" msgstr "" -#: ../duplicity-bin:796 +#: ../duplicity-bin:810 msgid "Which can't be deleted because newer sets depend on them." msgstr "" -#: ../duplicity-bin:800 +#: ../duplicity-bin:814 msgid "" "Current active backup chain is older than specified time. However, it will " "not be deleted. To remove all your backups, manually purge the repository." msgstr "" -#: ../duplicity-bin:806 +#: ../duplicity-bin:820 msgid "No old backup sets found, nothing deleted." msgstr "" -#: ../duplicity-bin:809 +#: ../duplicity-bin:823 msgid "Deleting backup set at time:" msgid_plural "Deleting backup sets at times:" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:826 +#: ../duplicity-bin:840 msgid "Found old backup set at the following time:" msgid_plural "Found old backup sets at the following times:" msgstr[0] "" msgstr[1] "" -#: ../duplicity-bin:830 +#: ../duplicity-bin:844 msgid "Rerun command with --force option to actually delete." msgstr "" -#: ../duplicity-bin:908 +#: ../duplicity-bin:922 #, python-format msgid "Deleting local %s (not authoritative at backend)." msgstr "" -#: ../duplicity-bin:912 ../duplicity/dup_temp.py:188 +#: ../duplicity-bin:926 ../duplicity/dup_temp.py:188 #, python-format msgid "Unable to delete %s: %s" msgstr "" -#: ../duplicity-bin:940 ../duplicity/dup_temp.py:252 +#: ../duplicity-bin:954 ../duplicity/dup_temp.py:252 #, python-format msgid "Failed to read %s: %s" msgstr "" -#: ../duplicity-bin:951 +#: ../duplicity-bin:965 #, python-format msgid "Copying %s to local cache." msgstr "" -#: ../duplicity-bin:999 +#: ../duplicity-bin:1013 msgid "Local and Remote metadata are synchronized, no sync needed." msgstr "" -#: ../duplicity-bin:1004 +#: ../duplicity-bin:1018 msgid "Synchronizing remote metadata to local cache..." msgstr "" -#: ../duplicity-bin:1017 +#: ../duplicity-bin:1031 msgid "Sync would copy the following from remote to local:" msgstr "" -#: ../duplicity-bin:1020 +#: ../duplicity-bin:1034 msgid "Sync would remove the following spurious local files:" msgstr "" -#: ../duplicity-bin:1063 +#: ../duplicity-bin:1077 msgid "Unable to get free space on temp." msgstr "" -#: ../duplicity-bin:1071 +#: ../duplicity-bin:1085 #, python-format msgid "Temp space has %d available, backup needs approx %d." msgstr "" -#: ../duplicity-bin:1074 +#: ../duplicity-bin:1088 #, python-format msgid "Temp has %d available, backup will use approx %d." msgstr "" -#: ../duplicity-bin:1082 +#: ../duplicity-bin:1096 msgid "Unable to get max open files." msgstr "" -#: ../duplicity-bin:1086 +#: ../duplicity-bin:1100 #, python-format msgid "" "Max open files of %s is too low, should be >= 1024.\n" "Use 'ulimit -n 1024' or higher to correct.\n" msgstr "" -#: ../duplicity-bin:1135 +#: ../duplicity-bin:1149 msgid "" "RESTART: The first volume failed to upload before termination.\n" " Restart is impossible...starting backup from beginning." msgstr "" -#: ../duplicity-bin:1141 +#: ../duplicity-bin:1155 #, python-format msgid "" "RESTART: Volumes %d to %d failed to upload before termination.\n" " Restarting backup at volume %d." msgstr "" -#: ../duplicity-bin:1148 +#: ../duplicity-bin:1162 #, python-format msgid "" "RESTART: Impossible backup state: manifest has %d vols, remote has %d vols.\n" @@ -272,43 +277,43 @@ msgid "" " backup then restart the backup from the beginning." msgstr "" -#: ../duplicity-bin:1219 +#: ../duplicity-bin:1233 #, python-format msgid "Last %s backup left a partial set, restarting." msgstr "" -#: ../duplicity-bin:1223 +#: ../duplicity-bin:1237 #, python-format msgid "Cleaning up previous partial %s backup set, restarting." msgstr "" -#: ../duplicity-bin:1234 +#: ../duplicity-bin:1248 msgid "Last full backup date:" msgstr "" -#: ../duplicity-bin:1236 +#: ../duplicity-bin:1250 msgid "Last full backup date: none" msgstr "" -#: ../duplicity-bin:1238 +#: ../duplicity-bin:1252 msgid "Last full backup is too old, forcing full backup" msgstr "" -#: ../duplicity-bin:1346 +#: ../duplicity-bin:1360 msgid "INT intercepted...exiting." msgstr "" -#: ../duplicity-bin:1352 +#: ../duplicity-bin:1366 #, python-format msgid "GPG error detail: %s" msgstr "" -#: ../duplicity-bin:1361 +#: ../duplicity-bin:1375 #, python-format msgid "User error detail: %s" msgstr "" -#: ../duplicity-bin:1370 +#: ../duplicity-bin:1384 #, python-format msgid "Backend error detail: %s" msgstr "" @@ -349,26 +354,26 @@ msgstr "" msgid "task execution done (success: %s)" msgstr "" -#: ../duplicity/backend.py:442 ../duplicity/backend.py:466 +#: ../duplicity/backend.py:471 ../duplicity/backend.py:495 #, python-format msgid "Reading results of '%s'" msgstr "" -#: ../duplicity/backend.py:481 +#: ../duplicity/backend.py:510 #, python-format msgid "Running '%s' failed with code %d (attempt #%d)" msgid_plural "Running '%s' failed with code %d (attempt #%d)" msgstr[0] "" msgstr[1] "" -#: ../duplicity/backend.py:485 +#: ../duplicity/backend.py:514 #, python-format msgid "" "Error is:\n" "%s" msgstr "" -#: ../duplicity/backend.py:487 +#: ../duplicity/backend.py:516 #, python-format msgid "Giving up trying to execute '%s' after %d attempt" msgid_plural "Giving up trying to execute '%s' after %d attempts" @@ -403,94 +408,94 @@ msgstr "" msgid "Added incremental Backupset (start_time: %s / end_time: %s)" msgstr "" -#: ../duplicity/collections.py:386 +#: ../duplicity/collections.py:389 msgid "Chain start time: " msgstr "" -#: ../duplicity/collections.py:387 +#: ../duplicity/collections.py:390 msgid "Chain end time: " msgstr "" -#: ../duplicity/collections.py:388 +#: ../duplicity/collections.py:391 #, python-format msgid "Number of contained backup sets: %d" msgstr "" -#: ../duplicity/collections.py:390 +#: ../duplicity/collections.py:393 #, python-format msgid "Total number of contained volumes: %d" msgstr "" -#: ../duplicity/collections.py:392 +#: ../duplicity/collections.py:395 msgid "Type of backup set:" msgstr "" -#: ../duplicity/collections.py:392 +#: ../duplicity/collections.py:395 msgid "Time:" msgstr "" -#: ../duplicity/collections.py:392 +#: ../duplicity/collections.py:395 msgid "Num volumes:" msgstr "" -#: ../duplicity/collections.py:396 +#: ../duplicity/collections.py:399 msgid "Full" msgstr "" -#: ../duplicity/collections.py:399 +#: ../duplicity/collections.py:402 msgid "Incremental" msgstr "" -#: ../duplicity/collections.py:459 +#: ../duplicity/collections.py:462 msgid "local" msgstr "" -#: ../duplicity/collections.py:461 +#: ../duplicity/collections.py:464 msgid "remote" msgstr "" -#: ../duplicity/collections.py:614 +#: ../duplicity/collections.py:617 msgid "Collection Status" msgstr "" -#: ../duplicity/collections.py:616 +#: ../duplicity/collections.py:619 #, python-format msgid "Connecting with backend: %s" msgstr "" -#: ../duplicity/collections.py:618 +#: ../duplicity/collections.py:621 #, python-format msgid "Archive dir: %s" msgstr "" -#: ../duplicity/collections.py:621 +#: ../duplicity/collections.py:624 #, python-format msgid "Found %d secondary backup chain." msgid_plural "Found %d secondary backup chains." msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:626 +#: ../duplicity/collections.py:629 #, python-format msgid "Secondary chain %d of %d:" msgstr "" -#: ../duplicity/collections.py:632 +#: ../duplicity/collections.py:635 msgid "Found primary backup chain with matching signature chain:" msgstr "" -#: ../duplicity/collections.py:636 +#: ../duplicity/collections.py:639 msgid "No backup chains with active signatures found" msgstr "" -#: ../duplicity/collections.py:639 +#: ../duplicity/collections.py:642 #, python-format msgid "Also found %d backup set not part of any chain," msgid_plural "Also found %d backup sets not part of any chain," msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:643 +#: ../duplicity/collections.py:646 #, python-format msgid "and %d incomplete backup set." msgid_plural "and %d incomplete backup sets." @@ -498,95 +503,95 @@ msgstr[0] "" msgstr[1] "" #. "cleanup" is a hard-coded command, so do not translate it -#: ../duplicity/collections.py:648 +#: ../duplicity/collections.py:651 msgid "These may be deleted by running duplicity with the \"cleanup\" command." msgstr "" -#: ../duplicity/collections.py:651 +#: ../duplicity/collections.py:654 msgid "No orphaned or incomplete backup sets found." msgstr "" -#: ../duplicity/collections.py:667 +#: ../duplicity/collections.py:670 #, python-format msgid "%d file exists on backend" msgid_plural "%d files exist on backend" msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:674 +#: ../duplicity/collections.py:677 #, python-format msgid "%d file exists in cache" msgid_plural "%d files exist in cache" msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:726 +#: ../duplicity/collections.py:729 msgid "Warning, discarding last backup set, because of missing signature file." msgstr "" -#: ../duplicity/collections.py:749 +#: ../duplicity/collections.py:752 msgid "Warning, found the following local orphaned signature file:" msgid_plural "Warning, found the following local orphaned signature files:" msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:758 +#: ../duplicity/collections.py:761 msgid "Warning, found the following remote orphaned signature file:" msgid_plural "Warning, found the following remote orphaned signature files:" msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:767 +#: ../duplicity/collections.py:770 msgid "Warning, found signatures but no corresponding backup files" msgstr "" -#: ../duplicity/collections.py:771 +#: ../duplicity/collections.py:774 msgid "" "Warning, found incomplete backup sets, probably left from aborted session" msgstr "" -#: ../duplicity/collections.py:775 +#: ../duplicity/collections.py:778 msgid "Warning, found the following orphaned backup file:" msgid_plural "Warning, found the following orphaned backup files:" msgstr[0] "" msgstr[1] "" -#: ../duplicity/collections.py:793 +#: ../duplicity/collections.py:796 #, python-format msgid "Extracting backup chains from list of files: %s" msgstr "" -#: ../duplicity/collections.py:803 +#: ../duplicity/collections.py:806 #, python-format msgid "File %s is part of known set" msgstr "" -#: ../duplicity/collections.py:806 +#: ../duplicity/collections.py:809 #, python-format msgid "File %s is not part of a known set; creating new set" msgstr "" -#: ../duplicity/collections.py:811 +#: ../duplicity/collections.py:814 #, python-format msgid "Ignoring file (rejected by backup set) '%s'" msgstr "" -#: ../duplicity/collections.py:824 +#: ../duplicity/collections.py:827 #, python-format msgid "Found backup chain %s" msgstr "" -#: ../duplicity/collections.py:829 +#: ../duplicity/collections.py:832 #, python-format msgid "Added set %s to pre-existing chain %s" msgstr "" -#: ../duplicity/collections.py:833 +#: ../duplicity/collections.py:836 #, python-format msgid "Found orphaned set %s" msgstr "" -#: ../duplicity/collections.py:985 +#: ../duplicity/collections.py:988 #, python-format msgid "" "No signature chain for the requested time. Using oldest available chain, " @@ -613,15 +618,15 @@ msgstr "" #. Used in usage help to represent a Unix-style path name. Example: #. --archive-dir #: ../duplicity/commandline.py:225 ../duplicity/commandline.py:233 -#: ../duplicity/commandline.py:250 ../duplicity/commandline.py:300 -#: ../duplicity/commandline.py:447 ../duplicity/commandline.py:662 +#: ../duplicity/commandline.py:250 ../duplicity/commandline.py:304 +#: ../duplicity/commandline.py:457 ../duplicity/commandline.py:672 msgid "path" msgstr "" #. Used in usage help to represent an ID for a GnuPG key. Example: #. --encrypt-key #: ../duplicity/commandline.py:245 ../duplicity/commandline.py:252 -#: ../duplicity/commandline.py:436 ../duplicity/commandline.py:635 +#: ../duplicity/commandline.py:446 ../duplicity/commandline.py:645 msgid "gpg-key-id" msgstr "" @@ -629,42 +634,42 @@ msgstr "" #. matching one or more files, as described in the documentation. #. Example: #. --exclude -#: ../duplicity/commandline.py:260 ../duplicity/commandline.py:340 -#: ../duplicity/commandline.py:685 +#: ../duplicity/commandline.py:260 ../duplicity/commandline.py:344 +#: ../duplicity/commandline.py:695 msgid "shell_pattern" msgstr "" #. Used in usage help to represent the name of a file. Example: #. --log-file #: ../duplicity/commandline.py:266 ../duplicity/commandline.py:273 -#: ../duplicity/commandline.py:278 ../duplicity/commandline.py:342 -#: ../duplicity/commandline.py:347 ../duplicity/commandline.py:358 -#: ../duplicity/commandline.py:631 +#: ../duplicity/commandline.py:278 ../duplicity/commandline.py:346 +#: ../duplicity/commandline.py:351 ../duplicity/commandline.py:362 +#: ../duplicity/commandline.py:641 msgid "filename" msgstr "" #. Used in usage help to represent a regular expression (regexp). -#: ../duplicity/commandline.py:285 ../duplicity/commandline.py:349 +#: ../duplicity/commandline.py:285 ../duplicity/commandline.py:353 msgid "regular_expression" msgstr "" #. Used in usage help to represent a time spec for a previous #. point in time, as described in the documentation. Example: #. duplicity remove-older-than time [options] target_url -#: ../duplicity/commandline.py:312 ../duplicity/commandline.py:402 -#: ../duplicity/commandline.py:717 +#: ../duplicity/commandline.py:316 ../duplicity/commandline.py:406 +#: ../duplicity/commandline.py:727 msgid "time" msgstr "" #. Used in usage help. (Should be consistent with the "Options:" #. header.) Example: #. duplicity [full|incremental] [options] source_dir target_url -#: ../duplicity/commandline.py:316 ../duplicity/commandline.py:444 -#: ../duplicity/commandline.py:650 +#: ../duplicity/commandline.py:320 ../duplicity/commandline.py:454 +#: ../duplicity/commandline.py:660 msgid "options" msgstr "" -#: ../duplicity/commandline.py:325 +#: ../duplicity/commandline.py:329 #, python-format msgid "" "Running in 'ignore errors' mode due to %s; please re-consider if this was " @@ -672,141 +677,141 @@ msgid "" msgstr "" #. Used in usage help to represent an imap mailbox -#: ../duplicity/commandline.py:338 +#: ../duplicity/commandline.py:342 msgid "imap_mailbox" msgstr "" -#: ../duplicity/commandline.py:352 +#: ../duplicity/commandline.py:356 msgid "file_descriptor" msgstr "" #. Used in usage help (noun) -#: ../duplicity/commandline.py:363 +#: ../duplicity/commandline.py:367 msgid "backup name" msgstr "" #. Used in usage help to represent a desired number of #. something. Example: #. --num-retries -#: ../duplicity/commandline.py:379 ../duplicity/commandline.py:477 -#: ../duplicity/commandline.py:645 +#: ../duplicity/commandline.py:383 ../duplicity/commandline.py:427 +#: ../duplicity/commandline.py:487 ../duplicity/commandline.py:655 msgid "number" msgstr "" #. noun -#: ../duplicity/commandline.py:422 ../duplicity/commandline.py:426 -#: ../duplicity/commandline.py:616 +#: ../duplicity/commandline.py:432 ../duplicity/commandline.py:436 +#: ../duplicity/commandline.py:626 msgid "command" msgstr "" #. Used in usage help. Example: #. --timeout -#: ../duplicity/commandline.py:452 ../duplicity/commandline.py:679 +#: ../duplicity/commandline.py:462 ../duplicity/commandline.py:689 msgid "seconds" msgstr "" #. abbreviation for "character" (noun) -#: ../duplicity/commandline.py:458 ../duplicity/commandline.py:613 +#: ../duplicity/commandline.py:468 ../duplicity/commandline.py:623 msgid "char" msgstr "" -#: ../duplicity/commandline.py:579 +#: ../duplicity/commandline.py:589 #, python-format msgid "Using archive dir: %s" msgstr "" -#: ../duplicity/commandline.py:580 +#: ../duplicity/commandline.py:590 #, python-format msgid "Using backup name: %s" msgstr "" -#: ../duplicity/commandline.py:587 +#: ../duplicity/commandline.py:597 #, python-format msgid "Command line error: %s" msgstr "" -#: ../duplicity/commandline.py:588 +#: ../duplicity/commandline.py:598 msgid "Enter 'duplicity --help' for help screen." msgstr "" #. Used in usage help to represent a Unix-style path name. Example: #. rsync://user[:password]@other_host[:port]//absolute_path -#: ../duplicity/commandline.py:601 +#: ../duplicity/commandline.py:611 msgid "absolute_path" msgstr "" #. Used in usage help. Example: #. tahoe://alias/some_dir -#: ../duplicity/commandline.py:605 +#: ../duplicity/commandline.py:615 msgid "alias" msgstr "" #. Used in help to represent a "bucket name" for Amazon Web #. Services' Simple Storage Service (S3). Example: #. s3://other.host/bucket_name[/prefix] -#: ../duplicity/commandline.py:610 +#: ../duplicity/commandline.py:620 msgid "bucket_name" msgstr "" #. Used in usage help to represent the name of a container in #. Amazon Web Services' Cloudfront. Example: #. cf+http://container_name -#: ../duplicity/commandline.py:621 +#: ../duplicity/commandline.py:631 msgid "container_name" msgstr "" #. noun -#: ../duplicity/commandline.py:624 +#: ../duplicity/commandline.py:634 msgid "count" msgstr "" #. Used in usage help to represent the name of a file directory -#: ../duplicity/commandline.py:627 +#: ../duplicity/commandline.py:637 msgid "directory" msgstr "" #. Used in usage help, e.g. to represent the name of a code #. module. Example: #. rsync://user[:password]@other.host[:port]::/module/some_dir -#: ../duplicity/commandline.py:640 +#: ../duplicity/commandline.py:650 msgid "module" msgstr "" #. Used in usage help to represent an internet hostname. Example: #. ftp://user[:password]@other.host[:port]/some_dir -#: ../duplicity/commandline.py:654 +#: ../duplicity/commandline.py:664 msgid "other.host" msgstr "" #. Used in usage help. Example: #. ftp://user[:password]@other.host[:port]/some_dir -#: ../duplicity/commandline.py:658 +#: ../duplicity/commandline.py:668 msgid "password" msgstr "" #. Used in usage help to represent a TCP port number. Example: #. ftp://user[:password]@other.host[:port]/some_dir -#: ../duplicity/commandline.py:666 +#: ../duplicity/commandline.py:676 msgid "port" msgstr "" #. Used in usage help. This represents a string to be used as a #. prefix to names for backup files created by Duplicity. Example: #. s3://other.host/bucket_name[/prefix] -#: ../duplicity/commandline.py:671 +#: ../duplicity/commandline.py:681 msgid "prefix" msgstr "" #. Used in usage help to represent a Unix-style path name. Example: #. rsync://user[:password]@other.host[:port]/relative_path -#: ../duplicity/commandline.py:675 +#: ../duplicity/commandline.py:685 msgid "relative_path" msgstr "" #. Used in usage help to represent the name of a single file #. directory or a Unix-style path to a directory. Example: #. file:///some_dir -#: ../duplicity/commandline.py:690 +#: ../duplicity/commandline.py:700 msgid "some_dir" msgstr "" @@ -814,14 +819,14 @@ msgstr "" #. directory or a Unix-style path to a directory where files will be #. coming FROM. Example: #. duplicity [full|incremental] [options] source_dir target_url -#: ../duplicity/commandline.py:696 +#: ../duplicity/commandline.py:706 msgid "source_dir" msgstr "" #. Used in usage help to represent a URL files will be coming #. FROM. Example: #. duplicity [restore] [options] source_url target_dir -#: ../duplicity/commandline.py:701 +#: ../duplicity/commandline.py:711 msgid "source_url" msgstr "" @@ -829,75 +834,75 @@ msgstr "" #. directory or a Unix-style path to a directory. where files will be #. going TO. Example: #. duplicity [restore] [options] source_url target_dir -#: ../duplicity/commandline.py:707 +#: ../duplicity/commandline.py:717 msgid "target_dir" msgstr "" #. Used in usage help to represent a URL files will be going TO. #. Example: #. duplicity [full|incremental] [options] source_dir target_url -#: ../duplicity/commandline.py:712 +#: ../duplicity/commandline.py:722 msgid "target_url" msgstr "" #. Used in usage help to represent a user name (i.e. login). #. Example: #. ftp://user[:password]@other.host[:port]/some_dir -#: ../duplicity/commandline.py:722 +#: ../duplicity/commandline.py:732 msgid "user" msgstr "" #. Header in usage help -#: ../duplicity/commandline.py:739 +#: ../duplicity/commandline.py:749 msgid "Backends and their URL formats:" msgstr "" #. Header in usage help -#: ../duplicity/commandline.py:761 +#: ../duplicity/commandline.py:771 msgid "Commands:" msgstr "" -#: ../duplicity/commandline.py:785 +#: ../duplicity/commandline.py:795 #, python-format msgid "Specified archive directory '%s' does not exist, or is not a directory" msgstr "" -#: ../duplicity/commandline.py:794 +#: ../duplicity/commandline.py:804 #, python-format msgid "" "Sign key should be an 8 character hex string, like 'AA0E73D2'.\n" "Received '%s' instead." msgstr "" -#: ../duplicity/commandline.py:852 +#: ../duplicity/commandline.py:862 #, python-format msgid "" "Restore destination directory %s already exists.\n" "Will not overwrite." msgstr "" -#: ../duplicity/commandline.py:857 +#: ../duplicity/commandline.py:867 #, python-format msgid "Verify directory %s does not exist" msgstr "" -#: ../duplicity/commandline.py:863 +#: ../duplicity/commandline.py:873 #, python-format msgid "Backup source directory %s does not exist." msgstr "" -#: ../duplicity/commandline.py:892 +#: ../duplicity/commandline.py:902 #, python-format msgid "Command line warning: %s" msgstr "" -#: ../duplicity/commandline.py:892 +#: ../duplicity/commandline.py:902 msgid "" "Selection options --exclude/--include\n" "currently work only when backing up,not restoring." msgstr "" -#: ../duplicity/commandline.py:940 +#: ../duplicity/commandline.py:950 #, python-format msgid "" "Bad URL '%s'.\n" @@ -905,7 +910,7 @@ msgid "" "\"file:///usr/local\". See the man page for more information." msgstr "" -#: ../duplicity/commandline.py:965 +#: ../duplicity/commandline.py:975 msgid "Main action: " msgstr "" diff --git a/po/update-pot b/po/update-pot index f8616cc..b448528 100755 --- a/po/update-pot +++ b/po/update-pot @@ -1,4 +1,4 @@ #!/bin/sh intltool-update --pot -g duplicity -sed -e 's/^#\. TRANSL:/#./' -i duplicity.pot +sed -i -e 's/^#\. TRANSL:/#./' duplicity.pot