Merge lp:~mterry/duplicity/require-2.6 into lp:duplicity/0.6

Proposed by Michael Terry
Status: Merged
Merged at revision: 971
Proposed branch: lp:~mterry/duplicity/require-2.6
Merge into: lp:duplicity/0.6
Prerequisite: lp:~mterry/duplicity/modern-testing
Diff against target: 4735 lines (+235/-3624)
24 files modified
README (+1/-2)
bin/duplicity.1 (+1/-1)
dist/duplicity.spec.template (+2/-2)
duplicity/__init__.py (+1/-8)
duplicity/_librsyncmodule.c (+0/-9)
duplicity/backend.py (+40/-50)
duplicity/backends/botobackend.py (+0/-4)
duplicity/backends/webdavbackend.py (+2/-2)
duplicity/commandline.py (+1/-3)
duplicity/log.py (+5/-24)
duplicity/tarfile.py (+33/-2592)
duplicity/urlparse_2_5.py (+0/-385)
po/POTFILES.in (+0/-1)
po/duplicity.pot (+125/-123)
setup.py (+2/-4)
tarfile-CHANGES (+0/-3)
tarfile-LICENSE (+0/-92)
testing/__init__.py (+0/-3)
testing/run-tests (+1/-1)
testing/run-tests-ve (+1/-1)
testing/tests/__init__.py (+0/-9)
testing/tests/test_parsedurl.py (+10/-1)
testing/tests/test_tarfile.py (+8/-300)
testing/tests/test_unicode.py (+2/-4)
To merge this branch: bzr merge lp:~mterry/duplicity/require-2.6
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+216210@code.launchpad.net

Description of the change

Require at least Python 2.6.

Our code base already requires 2.6, because 2.6-isms have crept in. Usually because we or a contributor didn't think to test with 2.4. And frankly, I'm not even sure how to test with 2.4 on a modern system [1].

You know I've been pushing for this change for a while, but it seems that at this point, it's just moving from de facto to de jure.

Benefits of this:
 - We can start using newer syntax and features
 - We can drop a bunch of code (notably our internal copies of urlparse and tarfile)

Most of this branch is just removing code that we kept around only for 2.4. I didn't start using any new 2.6-isms. Those can be separate branches if this is accepted.

[1] https://launchpad.net/~fkrull/+archive/deadsnakes is a good start, but virtualenv in Ubuntu 14.04 only supports 2.6+. So you'd have to hook everything up manually.

To post a comment you must log in.
Revision history for this message
Michael Terry (mterry) wrote :

/me hugs Ken

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README'
--- README 2014-02-21 17:35:24 +0000
+++ README 2014-04-16 20:51:42 +0000
@@ -19,7 +19,7 @@
1919
20REQUIREMENTS:20REQUIREMENTS:
2121
22 * Python v2.4 or later22 * Python v2.6 or later
23 * librsync v0.9.6 or later23 * librsync v0.9.6 or later
24 * GnuPG v1.x for encryption24 * GnuPG v1.x for encryption
25 * python-lockfile for concurrency locking25 * python-lockfile for concurrency locking
@@ -28,7 +28,6 @@
28 * for ftp over SSL -- lftp version 3.7.15 or later28 * for ftp over SSL -- lftp version 3.7.15 or later
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 access
32 * Boto 2.7.0 or later for Glacier S3 access31 * Boto 2.7.0 or later for Glacier S3 access
3332
34If you install from the source package, you will also need:33If you install from the source package, you will also need:
3534
=== modified file 'bin/duplicity.1'
--- bin/duplicity.1 2014-03-09 20:37:24 +0000
+++ bin/duplicity.1 2014-04-16 20:51:42 +0000
@@ -51,7 +51,7 @@
51.SH REQUIREMENTS51.SH REQUIREMENTS
52Duplicity requires a POSIX-like operating system with a52Duplicity requires a POSIX-like operating system with a
53.B python53.B python
54interpreter version 2.4+ installed.54interpreter version 2.6+ installed.
55It is best used under GNU/Linux.55It is best used under GNU/Linux.
5656
57Some backends also require additional components (probably available as packages for your specific platform):57Some backends also require additional components (probably available as packages for your specific platform):
5858
=== modified file 'dist/duplicity.spec.template'
--- dist/duplicity.spec.template 2011-11-25 17:47:57 +0000
+++ dist/duplicity.spec.template 2014-04-16 20:51:42 +0000
@@ -10,8 +10,8 @@
10License: GPL10License: GPL
11Group: Applications/Archiving11Group: Applications/Archiving
12BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)12BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
13requires: librsync >= 0.9.6, %{PYTHON_NAME} >= 2.4, gnupg >= 1.0.613requires: librsync >= 0.9.6, %{PYTHON_NAME} >= 2.6, gnupg >= 1.0.6
14BuildPrereq: %{PYTHON_NAME}-devel >= 2.4, librsync-devel >= 0.9.614BuildPrereq: %{PYTHON_NAME}-devel >= 2.6, librsync-devel >= 0.9.6
1515
16%description16%description
17Duplicity incrementally backs up files and directory by encrypting17Duplicity incrementally backs up files and directory by encrypting
1818
=== modified file 'duplicity/__init__.py'
--- duplicity/__init__.py 2013-12-27 06:39:00 +0000
+++ duplicity/__init__.py 2014-04-16 20:51:42 +0000
@@ -19,12 +19,5 @@
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 __builtin__
23import gettext22import gettext
2423gettext.install('duplicity', unicode=True, names=['ngettext'])
25t = gettext.translation('duplicity', fallback=True)
26t.install(unicode=True)
27
28# Once we can depend on python >=2.5, we can just use names='ngettext' above.
29# But for now, do the install manually.
30__builtin__.__dict__['ngettext'] = t.ungettext
3124
=== modified file 'duplicity/_librsyncmodule.c'
--- duplicity/_librsyncmodule.c 2013-01-17 16:17:42 +0000
+++ duplicity/_librsyncmodule.c 2014-04-16 20:51:42 +0000
@@ -26,15 +26,6 @@
26#include <librsync.h>26#include <librsync.h>
27#define RS_JOB_BLOCKSIZE 6553627#define RS_JOB_BLOCKSIZE 65536
2828
29/* Support Python 2.4 and 2.5 */
30#ifndef PyVarObject_HEAD_INIT
31 #define PyVarObject_HEAD_INIT(type, size) \
32 PyObject_HEAD_INIT(type) size,
33#endif
34#ifndef Py_TYPE
35 #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
36#endif
37
38static PyObject *librsyncError;29static PyObject *librsyncError;
3930
40/* Sets python error string from result */31/* Sets python error string from result */
4132
=== modified file 'duplicity/backend.py'
--- duplicity/backend.py 2014-02-07 19:04:51 +0000
+++ duplicity/backend.py 2014-04-16 20:51:42 +0000
@@ -32,13 +32,12 @@
32import getpass32import getpass
33import gettext33import gettext
34import urllib34import urllib
35import urlparse
3536
36from duplicity import dup_temp37from duplicity import dup_temp
37from duplicity import dup_threading
38from duplicity import file_naming38from duplicity import file_naming
39from duplicity import globals39from duplicity import globals
40from duplicity import log40from duplicity import log
41from duplicity import urlparse_2_5 as urlparser
42from duplicity import progress41from duplicity import progress
4342
44from duplicity.util import exception_traceback43from duplicity.util import exception_traceback
@@ -58,6 +57,28 @@
58_forced_backend = None57_forced_backend = None
59_backends = {}58_backends = {}
6059
60# These URL schemes have a backend with a notion of an RFC "network location".
61# The 'file' and 's3+http' schemes should not be in this list.
62# 'http' and 'https' are not actually used for duplicity backend urls, but are needed
63# in order to properly support urls returned from some webdav servers. adding them here
64# is a hack. we should instead not stomp on the url parsing module to begin with.
65#
66# This looks similar to urlparse's 'uses_netloc' list, but urlparse doesn't use
67# that list for parsing, only creating urls. And doesn't include our custom
68# schemes anyway. So we keep our own here for our own use.
69uses_netloc = ['ftp',
70 'ftps',
71 'hsi',
72 'rsync',
73 's3',
74 'u1',
75 'scp', 'ssh', 'sftp',
76 'webdav', 'webdavs',
77 'gdocs',
78 'http', 'https',
79 'imap', 'imaps',
80 'mega']
81
6182
62def import_backends():83def import_backends():
63 """84 """
@@ -165,47 +186,6 @@
165 raise BackendException(_("Could not initialize backend: %s") % str(sys.exc_info()[1]))186 raise BackendException(_("Could not initialize backend: %s") % str(sys.exc_info()[1]))
166187
167188
168_urlparser_initialized = False
169_urlparser_initialized_lock = dup_threading.threading_module().Lock()
170
171def _ensure_urlparser_initialized():
172 """
173 Ensure that the appropriate clobbering of variables in the
174 urlparser module has been done. In the future, the need for this
175 clobbering to begin with should preferably be eliminated.
176 """
177 def init():
178 global _urlparser_initialized
179
180 if not _urlparser_initialized:
181 # These URL schemes have a backend with a notion of an RFC "network location".
182 # The 'file' and 's3+http' schemes should not be in this list.
183 # 'http' and 'https' are not actually used for duplicity backend urls, but are needed
184 # in order to properly support urls returned from some webdav servers. adding them here
185 # is a hack. we should instead not stomp on the url parsing module to begin with.
186 #
187 # todo: eliminate the need for backend specific hacking here completely.
188 urlparser.uses_netloc = ['ftp',
189 'ftps',
190 'hsi',
191 'rsync',
192 's3',
193 'u1',
194 'scp', 'ssh', 'sftp',
195 'webdav', 'webdavs',
196 'gdocs',
197 'http', 'https',
198 'imap', 'imaps',
199 'mega']
200
201 # Do not transform or otherwise parse the URL path component.
202 urlparser.uses_query = []
203 urlparser.uses_fragm = []
204
205 _urlparser_initialized = True
206
207 dup_threading.with_lock(_urlparser_initialized_lock, init)
208
209class ParsedUrl:189class ParsedUrl:
210 """190 """
211 Parse the given URL as a duplicity backend URL.191 Parse the given URL as a duplicity backend URL.
@@ -219,7 +199,6 @@
219 """199 """
220 def __init__(self, url_string):200 def __init__(self, url_string):
221 self.url_string = url_string201 self.url_string = url_string
222 _ensure_urlparser_initialized()
223202
224 # While useful in some cases, the fact is that the urlparser makes203 # While useful in some cases, the fact is that the urlparser makes
225 # all the properties in the URL deferred or lazy. This means that204 # all the properties in the URL deferred or lazy. This means that
@@ -227,7 +206,7 @@
227 # problems here, so they will be caught early.206 # problems here, so they will be caught early.
228207
229 try:208 try:
230 pu = urlparser.urlparse(url_string)209 pu = urlparse.urlparse(url_string)
231 except Exception:210 except Exception:
232 raise InvalidBackendURL("Syntax error in: %s" % url_string)211 raise InvalidBackendURL("Syntax error in: %s" % url_string)
233212
@@ -273,26 +252,37 @@
273 self.port = None252 self.port = None
274 try:253 try:
275 self.port = pu.port254 self.port = pu.port
276 except Exception:255 except Exception: # not raised in python2.7+, just returns None
277 # old style rsync://host::[/]dest, are still valid, though they contain no port256 # old style rsync://host::[/]dest, are still valid, though they contain no port
278 if not ( self.scheme in ['rsync'] and re.search('::[^:]*$', self.url_string)):257 if not ( self.scheme in ['rsync'] and re.search('::[^:]*$', self.url_string)):
279 raise InvalidBackendURL("Syntax error (port) in: %s A%s B%s C%s" % (url_string, (self.scheme in ['rsync']), re.search('::[^:]+$', self.netloc), self.netloc ) )258 raise InvalidBackendURL("Syntax error (port) in: %s A%s B%s C%s" % (url_string, (self.scheme in ['rsync']), re.search('::[^:]+$', self.netloc), self.netloc ) )
280259
260 # Our URL system uses two slashes more than urlparse's does when using
261 # non-netloc URLs. And we want to make sure that if urlparse assuming
262 # a netloc where we don't want one, that we correct it.
263 if self.scheme not in uses_netloc:
264 if self.netloc:
265 self.path = '//' + self.netloc + self.path
266 self.netloc = ''
267 self.hostname = None
268 elif self.path.startswith('/'):
269 self.path = '//' + self.path
270
281 # This happens for implicit local paths.271 # This happens for implicit local paths.
282 if not pu.scheme:272 if not self.scheme:
283 return273 return
284274
285 # Our backends do not handle implicit hosts.275 # Our backends do not handle implicit hosts.
286 if pu.scheme in urlparser.uses_netloc and not pu.hostname:276 if self.scheme in uses_netloc and not self.hostname:
287 raise InvalidBackendURL("Missing hostname in a backend URL which "277 raise InvalidBackendURL("Missing hostname in a backend URL which "
288 "requires an explicit hostname: %s"278 "requires an explicit hostname: %s"
289 "" % (url_string))279 "" % (url_string))
290280
291 # Our backends do not handle implicit relative paths.281 # Our backends do not handle implicit relative paths.
292 if pu.scheme not in urlparser.uses_netloc and not pu.path.startswith('//'):282 if self.scheme not in uses_netloc and not self.path.startswith('//'):
293 raise InvalidBackendURL("missing // - relative paths not supported "283 raise InvalidBackendURL("missing // - relative paths not supported "
294 "for scheme %s: %s"284 "for scheme %s: %s"
295 "" % (pu.scheme, url_string))285 "" % (self.scheme, url_string))
296286
297 def geturl(self):287 def geturl(self):
298 return self.url_string288 return self.url_string
299289
=== modified file 'duplicity/backends/botobackend.py'
--- duplicity/backends/botobackend.py 2014-02-21 17:14:37 +0000
+++ duplicity/backends/botobackend.py 2014-04-16 20:51:42 +0000
@@ -22,14 +22,10 @@
2222
23import duplicity.backend23import duplicity.backend
24from duplicity import globals24from duplicity import globals
25import sys
26from _boto_multi import BotoBackend as BotoMultiUploadBackend25from _boto_multi import BotoBackend as BotoMultiUploadBackend
27from _boto_single import BotoBackend as BotoSingleUploadBackend26from _boto_single import BotoBackend as BotoSingleUploadBackend
2827
29if globals.s3_use_multiprocessing:28if globals.s3_use_multiprocessing:
30 if sys.version_info[:2] < (2, 6):
31 print "Sorry, S3 multiprocessing requires version 2.6 or later of python"
32 sys.exit(1)
33 duplicity.backend.register_backend("gs", BotoMultiUploadBackend)29 duplicity.backend.register_backend("gs", BotoMultiUploadBackend)
34 duplicity.backend.register_backend("s3", BotoMultiUploadBackend)30 duplicity.backend.register_backend("s3", BotoMultiUploadBackend)
35 duplicity.backend.register_backend("s3+http", BotoMultiUploadBackend)31 duplicity.backend.register_backend("s3+http", BotoMultiUploadBackend)
3632
=== modified file 'duplicity/backends/webdavbackend.py'
--- duplicity/backends/webdavbackend.py 2013-12-30 16:01:49 +0000
+++ duplicity/backends/webdavbackend.py 2014-04-16 20:51:42 +0000
@@ -26,13 +26,13 @@
26import re26import re
27import urllib27import urllib
28import urllib228import urllib2
29import urlparse
29import xml.dom.minidom30import xml.dom.minidom
3031
31import duplicity.backend32import duplicity.backend
32from duplicity import globals33from duplicity import globals
33from duplicity import log34from duplicity import log
34from duplicity.errors import * #@UnusedWildImport35from duplicity.errors import * #@UnusedWildImport
35from duplicity import urlparse_2_5 as urlparser
36from duplicity.backend import retry_fatal36from duplicity.backend import retry_fatal
3737
38class CustomMethodRequest(urllib2.Request):38class CustomMethodRequest(urllib2.Request):
@@ -332,7 +332,7 @@
332 @return: A matching filename, or None if the href did not match.332 @return: A matching filename, or None if the href did not match.
333 """333 """
334 raw_filename = self._getText(href.childNodes).strip()334 raw_filename = self._getText(href.childNodes).strip()
335 parsed_url = urlparser.urlparse(urllib.unquote(raw_filename))335 parsed_url = urlparse.urlparse(urllib.unquote(raw_filename))
336 filename = parsed_url.path336 filename = parsed_url.path
337 log.Debug("webdav path decoding and translation: "337 log.Debug("webdav path decoding and translation: "
338 "%s -> %s" % (raw_filename, filename))338 "%s -> %s" % (raw_filename, filename))
339339
=== modified file 'duplicity/commandline.py'
--- duplicity/commandline.py 2014-03-09 20:37:24 +0000
+++ duplicity/commandline.py 2014-04-16 20:51:42 +0000
@@ -507,9 +507,7 @@
507 parser.add_option("--s3_multipart_max_timeout", type="int", metavar=_("number"))507 parser.add_option("--s3_multipart_max_timeout", type="int", metavar=_("number"))
508508
509 # Option to allow the s3/boto backend use the multiprocessing version.509 # Option to allow the s3/boto backend use the multiprocessing version.
510 # By default it is off since it does not work for Python 2.4 or 2.5.510 parser.add_option("--s3-use-multiprocessing", action = "store_true")
511 if sys.version_info[:2] >= (2, 6):
512 parser.add_option("--s3-use-multiprocessing", action = "store_true")
513511
514 # scp command to use (ssh pexpect backend)512 # scp command to use (ssh pexpect backend)
515 parser.add_option("--scp-command", metavar = _("command"))513 parser.add_option("--scp-command", metavar = _("command"))
516514
=== modified file 'duplicity/log.py'
--- duplicity/log.py 2013-12-27 06:39:00 +0000
+++ duplicity/log.py 2014-04-16 20:51:42 +0000
@@ -49,7 +49,6 @@
49 return DupToLoggerLevel(verb)49 return DupToLoggerLevel(verb)
5050
51def LevelName(level):51def LevelName(level):
52 level = LoggerToDupLevel(level)
53 if level >= 9: return "DEBUG"52 if level >= 9: return "DEBUG"
54 elif level >= 5: return "INFO"53 elif level >= 5: return "INFO"
55 elif level >= 3: return "NOTICE"54 elif level >= 3: return "NOTICE"
@@ -59,12 +58,10 @@
59def Log(s, verb_level, code=1, extra=None, force_print=False):58def Log(s, verb_level, code=1, extra=None, force_print=False):
60 """Write s to stderr if verbosity level low enough"""59 """Write s to stderr if verbosity level low enough"""
61 global _logger60 global _logger
62 # controlLine is a terrible hack until duplicity depends on Python 2.5
63 # and its logging 'extra' keyword that allows a custom record dictionary.
64 if extra:61 if extra:
65 _logger.controlLine = '%d %s' % (code, extra)62 controlLine = '%d %s' % (code, extra)
66 else:63 else:
67 _logger.controlLine = '%d' % (code)64 controlLine = '%d' % (code)
68 if not s:65 if not s:
69 s = '' # If None is passed, standard logging would render it as 'None'66 s = '' # If None is passed, standard logging would render it as 'None'
7067
@@ -79,8 +76,9 @@
79 if not isinstance(s, unicode):76 if not isinstance(s, unicode):
80 s = s.decode("utf8", "replace")77 s = s.decode("utf8", "replace")
8178
82 _logger.log(DupToLoggerLevel(verb_level), s)79 _logger.log(DupToLoggerLevel(verb_level), s,
83 _logger.controlLine = None80 extra={'levelName': LevelName(verb_level),
81 'controlLine': controlLine})
8482
85 if force_print:83 if force_print:
86 _logger.setLevel(initial_level)84 _logger.setLevel(initial_level)
@@ -305,22 +303,6 @@
305 shutdown()303 shutdown()
306 sys.exit(code)304 sys.exit(code)
307305
308class DupLogRecord(logging.LogRecord):
309 """Custom log record that holds a message code"""
310 def __init__(self, controlLine, *args, **kwargs):
311 global _logger
312 logging.LogRecord.__init__(self, *args, **kwargs)
313 self.controlLine = controlLine
314 self.levelName = LevelName(self.levelno)
315
316class DupLogger(logging.Logger):
317 """Custom logger that creates special code-bearing records"""
318 # controlLine is a terrible hack until duplicity depends on Python 2.5
319 # and its logging 'extra' keyword that allows a custom record dictionary.
320 controlLine = None
321 def makeRecord(self, name, lvl, fn, lno, msg, args, exc_info, *argv, **kwargs):
322 return DupLogRecord(self.controlLine, name, lvl, fn, lno, msg, args, exc_info)
323
324class OutFilter(logging.Filter):306class OutFilter(logging.Filter):
325 """Filter that only allows warning or less important messages"""307 """Filter that only allows warning or less important messages"""
326 def filter(self, record):308 def filter(self, record):
@@ -337,7 +319,6 @@
337 if _logger:319 if _logger:
338 return320 return
339321
340 logging.setLoggerClass(DupLogger)
341 _logger = logging.getLogger("duplicity")322 _logger = logging.getLogger("duplicity")
342323
343 # Default verbosity allows notices and above324 # Default verbosity allows notices and above
344325
=== modified file 'duplicity/tarfile.py'
--- duplicity/tarfile.py 2013-10-05 15:11:55 +0000
+++ duplicity/tarfile.py 2014-04-16 20:51:42 +0000
@@ -1,2594 +1,35 @@
1#! /usr/bin/python2.71# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2# -*- coding: iso-8859-1 -*-2#
3#-------------------------------------------------------------------3# Copyright 2013 Michael Terry <mike@mterry.name>
4# tarfile.py4#
5#-------------------------------------------------------------------5# This file is part of duplicity.
6# Copyright (C) 2002 Lars Gustäbel <lars@gustaebel.de>6#
7# All rights reserved.7# Duplicity is free software; you can redistribute it and/or modify it
8#8# under the terms of the GNU General Public License as published by the
9# Permission is hereby granted, free of charge, to any person9# Free Software Foundation; either version 2 of the License, or (at your
10# obtaining a copy of this software and associated documentation10# option) any later version.
11# files (the "Software"), to deal in the Software without11#
12# restriction, including without limitation the rights to use,12# Duplicity is distributed in the hope that it will be useful, but
13# copy, modify, merge, publish, distribute, sublicense, and/or sell13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# copies of the Software, and to permit persons to whom the14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# Software is furnished to do so, subject to the following15# General Public License for more details.
16# conditions:16#
17#17# You should have received a copy of the GNU General Public License
18# The above copyright notice and this permission notice shall be18# along with duplicity; if not, write to the Free Software Foundation,
19# included in all copies or substantial portions of the Software.19# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20#20
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,21"""Like system tarfile but with caching."""
22# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES22
23# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND23from __future__ import absolute_import
24# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT24
25# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,25import tarfile
26# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING26
27# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR27# Grab all symbols in tarfile, to try to reproduce its API exactly.
28# OTHER DEALINGS IN THE SOFTWARE.28# from <> import * wouldn't get everything we want, since tarfile defines
29#29# __all__. So we do it ourselves.
30"""Read from and write to tar format archives.30for sym in dir(tarfile):
31"""31 globals()[sym] = getattr(tarfile, sym)
3232
33__version__ = "$Revision: 85213 $"33# Now make sure that we cache the grp/pwd ops
34# $Source$
35
36version = "0.9.0"
37__author__ = "Lars Gustäbel (lars@gustaebel.de)"
38__date__ = "$Date: 2010-10-04 10:37:53 -0500 (Mon, 04 Oct 2010) $"
39__cvsid__ = "$Id: tarfile.py 85213 2010-10-04 15:37:53Z lars.gustaebel $"
40__credits__ = "Gustavo Niemeyer, Niels Gustäbel, Richard Townsend."
41
42#---------
43# Imports
44#---------
45import sys
46import os
47import shutil
48import stat
49import errno
50import time
51import struct
52import copy
53import re
54import operator
55
56from duplicity import cached_ops34from duplicity import cached_ops
57grp = pwd = cached_ops35grp = pwd = cached_ops
58
59# from tarfile import *
60__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError"]
61
62#---------------------------------------------------------
63# tar constants
64#---------------------------------------------------------
65NUL = "\0" # the null character
66BLOCKSIZE = 512 # length of processing blocks
67RECORDSIZE = BLOCKSIZE * 20 # length of records
68GNU_MAGIC = "ustar \0" # magic gnu tar string
69POSIX_MAGIC = "ustar\x0000" # magic posix tar string
70
71LENGTH_NAME = 100 # maximum length of a filename
72LENGTH_LINK = 100 # maximum length of a linkname
73LENGTH_PREFIX = 155 # maximum length of the prefix field
74
75REGTYPE = "0" # regular file
76AREGTYPE = "\0" # regular file
77LNKTYPE = "1" # link (inside tarfile)
78SYMTYPE = "2" # symbolic link
79CHRTYPE = "3" # character special device
80BLKTYPE = "4" # block special device
81DIRTYPE = "5" # directory
82FIFOTYPE = "6" # fifo special device
83CONTTYPE = "7" # contiguous file
84
85GNUTYPE_LONGNAME = "L" # GNU tar longname
86GNUTYPE_LONGLINK = "K" # GNU tar longlink
87GNUTYPE_SPARSE = "S" # GNU tar sparse file
88
89XHDTYPE = "x" # POSIX.1-2001 extended header
90XGLTYPE = "g" # POSIX.1-2001 global header
91SOLARIS_XHDTYPE = "X" # Solaris extended header
92
93USTAR_FORMAT = 0 # POSIX.1-1988 (ustar) format
94GNU_FORMAT = 1 # GNU tar format
95PAX_FORMAT = 2 # POSIX.1-2001 (pax) format
96DEFAULT_FORMAT = GNU_FORMAT
97
98#---------------------------------------------------------
99# tarfile constants
100#---------------------------------------------------------
101# File types that tarfile supports:
102SUPPORTED_TYPES = (REGTYPE, AREGTYPE, LNKTYPE,
103 SYMTYPE, DIRTYPE, FIFOTYPE,
104 CONTTYPE, CHRTYPE, BLKTYPE,
105 GNUTYPE_LONGNAME, GNUTYPE_LONGLINK,
106 GNUTYPE_SPARSE)
107
108# File types that will be treated as a regular file.
109REGULAR_TYPES = (REGTYPE, AREGTYPE,
110 CONTTYPE, GNUTYPE_SPARSE)
111
112# File types that are part of the GNU tar format.
113GNU_TYPES = (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK,
114 GNUTYPE_SPARSE)
115
116# Fields from a pax header that override a TarInfo attribute.
117PAX_FIELDS = ("path", "linkpath", "size", "mtime",
118 "uid", "gid", "uname", "gname")
119
120# Fields in a pax header that are numbers, all other fields
121# are treated as strings.
122PAX_NUMBER_FIELDS = {
123 "atime": float,
124 "ctime": float,
125 "mtime": float,
126 "uid": int,
127 "gid": int,
128 "size": int
129}
130
131#---------------------------------------------------------
132# Bits used in the mode field, values in octal.
133#---------------------------------------------------------
134S_IFLNK = 0120000 # symbolic link
135S_IFREG = 0100000 # regular file
136S_IFBLK = 0060000 # block device
137S_IFDIR = 0040000 # directory
138S_IFCHR = 0020000 # character device
139S_IFIFO = 0010000 # fifo
140
141TSUID = 04000 # set UID on execution
142TSGID = 02000 # set GID on execution
143TSVTX = 01000 # reserved
144
145TUREAD = 0400 # read by owner
146TUWRITE = 0200 # write by owner
147TUEXEC = 0100 # execute/search by owner
148TGREAD = 0040 # read by group
149TGWRITE = 0020 # write by group
150TGEXEC = 0010 # execute/search by group
151TOREAD = 0004 # read by other
152TOWRITE = 0002 # write by other
153TOEXEC = 0001 # execute/search by other
154
155#---------------------------------------------------------
156# initialization
157#---------------------------------------------------------
158ENCODING = sys.getfilesystemencoding()
159if ENCODING is None:
160 ENCODING = sys.getdefaultencoding()
161
162#---------------------------------------------------------
163# Some useful functions
164#---------------------------------------------------------
165
166def stn(s, length):
167 """Convert a python string to a null-terminated string buffer.
168 """
169 return s[:length] + (length - len(s)) * NUL
170
171def nts(s):
172 """Convert a null-terminated string field to a python string.
173 """
174 # Use the string up to the first null char.
175 p = s.find("\0")
176 if p == -1:
177 return s
178 return s[:p]
179
180def nti(s):
181 """Convert a number field to a python number.
182 """
183 # There are two possible encodings for a number field, see
184 # itn() below.
185 if s[0] != chr(0200):
186 try:
187 n = int(nts(s) or "0", 8)
188 except ValueError:
189 raise InvalidHeaderError("invalid header")
190 else:
191 n = 0L
192 for i in xrange(len(s) - 1):
193 n <<= 8
194 n += ord(s[i + 1])
195 return n
196
197def itn(n, digits=8, format=DEFAULT_FORMAT):
198 """Convert a python number to a number field.
199 """
200 # POSIX 1003.1-1988 requires numbers to be encoded as a string of
201 # octal digits followed by a null-byte, this allows values up to
202 # (8**(digits-1))-1. GNU tar allows storing numbers greater than
203 # that if necessary. A leading 0200 byte indicates this particular
204 # encoding, the following digits-1 bytes are a big-endian
205 # representation. This allows values up to (256**(digits-1))-1.
206 if 0 <= n < 8 ** (digits - 1):
207 s = "%0*o" % (digits - 1, n) + NUL
208 else:
209 if format != GNU_FORMAT or n >= 256 ** (digits - 1):
210 raise ValueError("overflow in number field")
211
212 if n < 0:
213 # XXX We mimic GNU tar's behaviour with negative numbers,
214 # this could raise OverflowError.
215 n = struct.unpack("L", struct.pack("l", n))[0]
216
217 s = ""
218 for i in xrange(digits - 1):
219 s = chr(n & 0377) + s
220 n >>= 8
221 s = chr(0200) + s
222 return s
223
224def uts(s, encoding, errors):
225 """Convert a unicode object to a string.
226 """
227 if errors == "utf-8":
228 # An extra error handler similar to the -o invalid=UTF-8 option
229 # in POSIX.1-2001. Replace untranslatable characters with their
230 # UTF-8 representation.
231 try:
232 return s.encode(encoding, "strict")
233 except UnicodeEncodeError:
234 x = []
235 for c in s:
236 try:
237 x.append(c.encode(encoding, "strict"))
238 except UnicodeEncodeError:
239 x.append(c.encode("utf8"))
240 return "".join(x)
241 else:
242 return s.encode(encoding, errors)
243
244def calc_chksums(buf):
245 """Calculate the checksum for a member's header by summing up all
246 characters except for the chksum field which is treated as if
247 it was filled with spaces. According to the GNU tar sources,
248 some tars (Sun and NeXT) calculate chksum with signed char,
249 which will be different if there are chars in the buffer with
250 the high bit set. So we calculate two checksums, unsigned and
251 signed.
252 """
253 unsigned_chksum = 256 + sum(struct.unpack("148B", buf[:148]) + struct.unpack("356B", buf[156:512]))
254 signed_chksum = 256 + sum(struct.unpack("148b", buf[:148]) + struct.unpack("356b", buf[156:512]))
255 return unsigned_chksum, signed_chksum
256
257def copyfileobj(src, dst, length=None):
258 """Copy length bytes from fileobj src to fileobj dst.
259 If length is None, copy the entire content.
260 """
261 if length == 0:
262 return
263 if length is None:
264 shutil.copyfileobj(src, dst)
265 return
266
267 BUFSIZE = 16 * 1024
268 blocks, remainder = divmod(length, BUFSIZE)
269 for b in xrange(blocks):
270 buf = src.read(BUFSIZE)
271 if len(buf) < BUFSIZE:
272 raise IOError("end of file reached")
273 dst.write(buf)
274
275 if remainder != 0:
276 buf = src.read(remainder)
277 if len(buf) < remainder:
278 raise IOError("end of file reached")
279 dst.write(buf)
280 return
281
282filemode_table = (
283 ((S_IFLNK, "l"),
284 (S_IFREG, "-"),
285 (S_IFBLK, "b"),
286 (S_IFDIR, "d"),
287 (S_IFCHR, "c"),
288 (S_IFIFO, "p")),
289
290 ((TUREAD, "r"),),
291 ((TUWRITE, "w"),),
292 ((TUEXEC|TSUID, "s"),
293 (TSUID, "S"),
294 (TUEXEC, "x")),
295
296 ((TGREAD, "r"),),
297 ((TGWRITE, "w"),),
298 ((TGEXEC|TSGID, "s"),
299 (TSGID, "S"),
300 (TGEXEC, "x")),
301
302 ((TOREAD, "r"),),
303 ((TOWRITE, "w"),),
304 ((TOEXEC|TSVTX, "t"),
305 (TSVTX, "T"),
306 (TOEXEC, "x"))
307)
308
309def filemode(mode):
310 """Convert a file's mode to a string of the form
311 -rwxrwxrwx.
312 Used by TarFile.list()
313 """
314 perm = []
315 for table in filemode_table:
316 for bit, char in table:
317 if mode & bit == bit:
318 perm.append(char)
319 break
320 else:
321 perm.append("-")
322 return "".join(perm)
323
324class TarError(Exception):
325 """Base exception."""
326 pass
327class ExtractError(TarError):
328 """General exception for extract errors."""
329 pass
330class ReadError(TarError):
331 """Exception for unreadble tar archives."""
332 pass
333class CompressionError(TarError):
334 """Exception for unavailable compression methods."""
335 pass
336class StreamError(TarError):
337 """Exception for unsupported operations on stream-like TarFiles."""
338 pass
339class HeaderError(TarError):
340 """Base exception for header errors."""
341 pass
342class EmptyHeaderError(HeaderError):
343 """Exception for empty headers."""
344 pass
345class TruncatedHeaderError(HeaderError):
346 """Exception for truncated headers."""
347 pass
348class EOFHeaderError(HeaderError):
349 """Exception for end of file headers."""
350 pass
351class InvalidHeaderError(HeaderError):
352 """Exception for invalid headers."""
353 pass
354class SubsequentHeaderError(HeaderError):
355 """Exception for missing and invalid extended headers."""
356 pass
357
358#---------------------------
359# internal stream interface
360#---------------------------
361class _LowLevelFile:
362 """Low-level file object. Supports reading and writing.
363 It is used instead of a regular file object for streaming
364 access.
365 """
366
367 def __init__(self, name, mode):
368 mode = {
369 "r": os.O_RDONLY,
370 "w": os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
371 }[mode]
372 if hasattr(os, "O_BINARY"):
373 mode |= os.O_BINARY
374 self.fd = os.open(name, mode, 0666)
375
376 def close(self):
377 os.close(self.fd)
378
379 def read(self, size):
380 return os.read(self.fd, size)
381
382 def write(self, s):
383 os.write(self.fd, s)
384
385class _Stream:
386 """Class that serves as an adapter between TarFile and
387 a stream-like object. The stream-like object only
388 needs to have a read() or write() method and is accessed
389 blockwise. Use of gzip or bzip2 compression is possible.
390 A stream-like object could be for example: sys.stdin,
391 sys.stdout, a socket, a tape device etc.
392
393 _Stream is intended to be used only internally.
394 """
395
396 def __init__(self, name, mode, comptype, fileobj, bufsize):
397 """Construct a _Stream object.
398 """
399 self._extfileobj = True
400 if fileobj is None:
401 fileobj = _LowLevelFile(name, mode)
402 self._extfileobj = False
403
404 if comptype == '*':
405 # Enable transparent compression detection for the
406 # stream interface
407 fileobj = _StreamProxy(fileobj)
408 comptype = fileobj.getcomptype()
409
410 self.name = name or ""
411 self.mode = mode
412 self.comptype = comptype
413 self.fileobj = fileobj
414 self.bufsize = bufsize
415 self.buf = ""
416 self.pos = 0L
417 self.closed = False
418
419 if comptype == "gz":
420 try:
421 import zlib
422 except ImportError:
423 raise CompressionError("zlib module is not available")
424 self.zlib = zlib
425 self.crc = zlib.crc32("") & 0xffffffffL
426 if mode == "r":
427 self._init_read_gz()
428 else:
429 self._init_write_gz()
430
431 if comptype == "bz2":
432 try:
433 import bz2
434 except ImportError:
435 raise CompressionError("bz2 module is not available")
436 if mode == "r":
437 self.dbuf = ""
438 self.cmp = bz2.BZ2Decompressor()
439 else:
440 self.cmp = bz2.BZ2Compressor()
441
442 def __del__(self):
443 if hasattr(self, "closed") and not self.closed:
444 self.close()
445
446 def _init_write_gz(self):
447 """Initialize for writing with gzip compression.
448 """
449 self.cmp = self.zlib.compressobj(9, self.zlib.DEFLATED,
450 -self.zlib.MAX_WBITS,
451 self.zlib.DEF_MEM_LEVEL,
452 0)
453 timestamp = struct.pack("<L", long(time.time()))
454 self.__write("\037\213\010\010%s\002\377" % timestamp)
455 if self.name.endswith(".gz"):
456 self.name = self.name[:-3]
457 self.__write(self.name + NUL)
458
459 def write(self, s):
460 """Write string s to the stream.
461 """
462 if self.comptype == "gz":
463 self.crc = self.zlib.crc32(s, self.crc) & 0xffffffffL
464 self.pos += len(s)
465 if self.comptype != "tar":
466 s = self.cmp.compress(s)
467 self.__write(s)
468
469 def __write(self, s):
470 """Write string s to the stream if a whole new block
471 is ready to be written.
472 """
473 self.buf += s
474 while len(self.buf) > self.bufsize:
475 self.fileobj.write(self.buf[:self.bufsize])
476 self.buf = self.buf[self.bufsize:]
477
478 def close(self):
479 """Close the _Stream object. No operation should be
480 done on it afterwards.
481 """
482 if self.closed:
483 return
484
485 if self.mode == "w" and self.comptype != "tar":
486 self.buf += self.cmp.flush()
487
488 if self.mode == "w" and self.buf:
489 self.fileobj.write(self.buf)
490 self.buf = ""
491 if self.comptype == "gz":
492 # The native zlib crc is an unsigned 32-bit integer, but
493 # the Python wrapper implicitly casts that to a signed C
494 # long. So, on a 32-bit box self.crc may "look negative",
495 # while the same crc on a 64-bit box may "look positive".
496 # To avoid irksome warnings from the `struct` module, force
497 # it to look positive on all boxes.
498 self.fileobj.write(struct.pack("<L", self.crc & 0xffffffffL))
499 self.fileobj.write(struct.pack("<L", self.pos & 0xffffFFFFL))
500
501 if not self._extfileobj:
502 self.fileobj.close()
503
504 self.closed = True
505
506 def _init_read_gz(self):
507 """Initialize for reading a gzip compressed fileobj.
508 """
509 self.cmp = self.zlib.decompressobj(-self.zlib.MAX_WBITS)
510 self.dbuf = ""
511
512 # taken from gzip.GzipFile with some alterations
513 if self.__read(2) != "\037\213":
514 raise ReadError("not a gzip file")
515 if self.__read(1) != "\010":
516 raise CompressionError("unsupported compression method")
517
518 flag = ord(self.__read(1))
519 self.__read(6)
520
521 if flag & 4:
522 xlen = ord(self.__read(1)) + 256 * ord(self.__read(1))
523 self.read(xlen)
524 if flag & 8:
525 while True:
526 s = self.__read(1)
527 if not s or s == NUL:
528 break
529 if flag & 16:
530 while True:
531 s = self.__read(1)
532 if not s or s == NUL:
533 break
534 if flag & 2:
535 self.__read(2)
536
537 def tell(self):
538 """Return the stream's file pointer position.
539 """
540 return self.pos
541
542 def seek(self, pos=0):
543 """Set the stream's file pointer to pos. Negative seeking
544 is forbidden.
545 """
546 if pos - self.pos >= 0:
547 blocks, remainder = divmod(pos - self.pos, self.bufsize)
548 for i in xrange(blocks):
549 self.read(self.bufsize)
550 self.read(remainder)
551 else:
552 raise StreamError("seeking backwards is not allowed")
553 return self.pos
554
555 def read(self, size=None):
556 """Return the next size number of bytes from the stream.
557 If size is not defined, return all bytes of the stream
558 up to EOF.
559 """
560 if size is None:
561 t = []
562 while True:
563 buf = self._read(self.bufsize)
564 if not buf:
565 break
566 t.append(buf)
567 buf = "".join(t)
568 else:
569 buf = self._read(size)
570 self.pos += len(buf)
571 return buf
572
573 def _read(self, size):
574 """Return size bytes from the stream.
575 """
576 if self.comptype == "tar":
577 return self.__read(size)
578
579 c = len(self.dbuf)
580 t = [self.dbuf]
581 while c < size:
582 buf = self.__read(self.bufsize)
583 if not buf:
584 break
585 try:
586 buf = self.cmp.decompress(buf)
587 except IOError:
588 raise ReadError("invalid compressed data")
589 t.append(buf)
590 c += len(buf)
591 t = "".join(t)
592 self.dbuf = t[size:]
593 return t[:size]
594
595 def __read(self, size):
596 """Return size bytes from stream. If internal buffer is empty,
597 read another block from the stream.
598 """
599 c = len(self.buf)
600 t = [self.buf]
601 while c < size:
602 buf = self.fileobj.read(self.bufsize)
603 if not buf:
604 break
605 t.append(buf)
606 c += len(buf)
607 t = "".join(t)
608 self.buf = t[size:]
609 return t[:size]
610# class _Stream
611
612class _StreamProxy(object):
613 """Small proxy class that enables transparent compression
614 detection for the Stream interface (mode 'r|*').
615 """
616
617 def __init__(self, fileobj):
618 self.fileobj = fileobj
619 self.buf = self.fileobj.read(BLOCKSIZE)
620
621 def read(self, size):
622 self.read = self.fileobj.read
623 return self.buf
624
625 def getcomptype(self):
626 if self.buf.startswith("\037\213\010"):
627 return "gz"
628 if self.buf.startswith("BZh91"):
629 return "bz2"
630 return "tar"
631
632 def close(self):
633 self.fileobj.close()
634# class StreamProxy
635
636class _BZ2Proxy(object):
637 """Small proxy class that enables external file object
638 support for "r:bz2" and "w:bz2" modes. This is actually
639 a workaround for a limitation in bz2 module's BZ2File
640 class which (unlike gzip.GzipFile) has no support for
641 a file object argument.
642 """
643
644 blocksize = 16 * 1024
645
646 def __init__(self, fileobj, mode):
647 self.fileobj = fileobj
648 self.mode = mode
649 self.name = getattr(self.fileobj, "name", None)
650 self.init()
651
652 def init(self):
653 import bz2
654 self.pos = 0
655 if self.mode == "r":
656 self.bz2obj = bz2.BZ2Decompressor()
657 self.fileobj.seek(0)
658 self.buf = ""
659 else:
660 self.bz2obj = bz2.BZ2Compressor()
661
662 def read(self, size):
663 b = [self.buf]
664 x = len(self.buf)
665 while x < size:
666 raw = self.fileobj.read(self.blocksize)
667 if not raw:
668 break
669 data = self.bz2obj.decompress(raw)
670 b.append(data)
671 x += len(data)
672 self.buf = "".join(b)
673
674 buf = self.buf[:size]
675 self.buf = self.buf[size:]
676 self.pos += len(buf)
677 return buf
678
679 def seek(self, pos):
680 if pos < self.pos:
681 self.init()
682 self.read(pos - self.pos)
683
684 def tell(self):
685 return self.pos
686
687 def write(self, data):
688 self.pos += len(data)
689 raw = self.bz2obj.compress(data)
690 self.fileobj.write(raw)
691
692 def close(self):
693 if self.mode == "w":
694 raw = self.bz2obj.flush()
695 self.fileobj.write(raw)
696# class _BZ2Proxy
697
698#------------------------
699# Extraction file object
700#------------------------
701class _FileInFile(object):
702 """A thin wrapper around an existing file object that
703 provides a part of its data as an individual file
704 object.
705 """
706
707 def __init__(self, fileobj, offset, size, sparse=None):
708 self.fileobj = fileobj
709 self.offset = offset
710 self.size = size
711 self.sparse = sparse
712 self.position = 0
713
714 def tell(self):
715 """Return the current file position.
716 """
717 return self.position
718
719 def seek(self, position):
720 """Seek to a position in the file.
721 """
722 self.position = position
723
724 def read(self, size=None):
725 """Read data from the file.
726 """
727 if size is None:
728 size = self.size - self.position
729 else:
730 size = min(size, self.size - self.position)
731
732 if self.sparse is None:
733 return self.readnormal(size)
734 else:
735 return self.readsparse(size)
736
737 def readnormal(self, size):
738 """Read operation for regular files.
739 """
740 self.fileobj.seek(self.offset + self.position)
741 self.position += size
742 return self.fileobj.read(size)
743
744 def readsparse(self, size):
745 """Read operation for sparse files.
746 """
747 data = []
748 while size > 0:
749 buf = self.readsparsesection(size)
750 if not buf:
751 break
752 size -= len(buf)
753 data.append(buf)
754 return "".join(data)
755
756 def readsparsesection(self, size):
757 """Read a single section of a sparse file.
758 """
759 section = self.sparse.find(self.position)
760
761 if section is None:
762 return ""
763
764 size = min(size, section.offset + section.size - self.position)
765
766 if isinstance(section, _data):
767 realpos = section.realpos + self.position - section.offset
768 self.fileobj.seek(self.offset + realpos)
769 self.position += size
770 return self.fileobj.read(size)
771 else:
772 self.position += size
773 return NUL * size
774#class _FileInFile
775
776
777class ExFileObject(object):
778 """File-like object for reading an archive member.
779 Is returned by TarFile.extractfile().
780 """
781 blocksize = 1024
782
783 def __init__(self, tarfile, tarinfo):
784 self.fileobj = _FileInFile(tarfile.fileobj,
785 tarinfo.offset_data,
786 tarinfo.size,
787 getattr(tarinfo, "sparse", None))
788 self.name = tarinfo.name
789 self.mode = "r"
790 self.closed = False
791 self.size = tarinfo.size
792
793 self.position = 0
794 self.buffer = ""
795
796 def read(self, size=None):
797 """Read at most size bytes from the file. If size is not
798 present or None, read all data until EOF is reached.
799 """
800 if self.closed:
801 raise ValueError("I/O operation on closed file")
802
803 buf = ""
804 if self.buffer:
805 if size is None:
806 buf = self.buffer
807 self.buffer = ""
808 else:
809 buf = self.buffer[:size]
810 self.buffer = self.buffer[size:]
811
812 if size is None:
813 buf += self.fileobj.read()
814 else:
815 buf += self.fileobj.read(size - len(buf))
816
817 self.position += len(buf)
818 return buf
819
820 def readline(self, size=-1):
821 """Read one entire line from the file. If size is present
822 and non-negative, return a string with at most that
823 size, which may be an incomplete line.
824 """
825 if self.closed:
826 raise ValueError("I/O operation on closed file")
827
828 if "\n" in self.buffer:
829 pos = self.buffer.find("\n") + 1
830 else:
831 buffers = [self.buffer]
832 while True:
833 buf = self.fileobj.read(self.blocksize)
834 buffers.append(buf)
835 if not buf or "\n" in buf:
836 self.buffer = "".join(buffers)
837 pos = self.buffer.find("\n") + 1
838 if pos == 0:
839 # no newline found.
840 pos = len(self.buffer)
841 break
842
843 if size != -1:
844 pos = min(size, pos)
845
846 buf = self.buffer[:pos]
847 self.buffer = self.buffer[pos:]
848 self.position += len(buf)
849 return buf
850
851 def readlines(self):
852 """Return a list with all remaining lines.
853 """
854 result = []
855 while True:
856 line = self.readline()
857 if not line: break
858 result.append(line)
859 return result
860
861 def tell(self):
862 """Return the current file position.
863 """
864 if self.closed:
865 raise ValueError("I/O operation on closed file")
866
867 return self.position
868
869 def seek(self, pos, whence=0):
870 """Seek to a position in the file.
871 """
872 if self.closed:
873 raise ValueError("I/O operation on closed file")
874
875 if whence == 0:
876 self.position = min(max(pos, 0), self.size)
877 elif whence == 1:
878 if pos < 0:
879 self.position = max(self.position + pos, 0)
880 else:
881 self.position = min(self.position + pos, self.size)
882 elif whence == 2:
883 self.position = max(min(self.size + pos, self.size), 0)
884 else:
885 raise ValueError("Invalid argument")
886
887 self.buffer = ""
888 self.fileobj.seek(self.position)
889
890 def close(self):
891 """Close the file object.
892 """
893 self.closed = True
894
895 def __iter__(self):
896 """Get an iterator over the file's lines.
897 """
898 while True:
899 line = self.readline()
900 if not line:
901 break
902 yield line
903#class ExFileObject
904
905#------------------
906# Exported Classes
907#------------------
908class TarInfo(object):
909 """Informational class which holds the details about an
910 archive member given by a tar header block.
911 TarInfo objects are returned by TarFile.getmember(),
912 TarFile.getmembers() and TarFile.gettarinfo() and are
913 usually created internally.
914 """
915
916 def __init__(self, name=""):
917 """Construct a TarInfo object. name is the optional name
918 of the member.
919 """
920 self.name = name # member name
921 self.mode = 0644 # file permissions
922 self.uid = 0 # user id
923 self.gid = 0 # group id
924 self.size = 0 # file size
925 self.mtime = 0 # modification time
926 self.chksum = 0 # header checksum
927 self.type = REGTYPE # member type
928 self.linkname = "" # link name
929 self.uname = "" # user name
930 self.gname = "" # group name
931 self.devmajor = 0 # device major number
932 self.devminor = 0 # device minor number
933
934 self.offset = 0 # the tar header starts here
935 self.offset_data = 0 # the file's data starts here
936
937 self.pax_headers = {} # pax header information
938
939 # In pax headers the "name" and "linkname" field are called
940 # "path" and "linkpath".
941 def _getpath(self):
942 return self.name
943 def _setpath(self, name):
944 self.name = name
945 path = property(_getpath, _setpath)
946
947 def _getlinkpath(self):
948 return self.linkname
949 def _setlinkpath(self, linkname):
950 self.linkname = linkname
951 linkpath = property(_getlinkpath, _setlinkpath)
952
953 def __repr__(self):
954 return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self))
955
956 def get_info(self, encoding, errors):
957 """Return the TarInfo's attributes as a dictionary.
958 """
959 info = {
960 "name": self.name,
961 "mode": self.mode & 07777,
962 "uid": self.uid,
963 "gid": self.gid,
964 "size": self.size,
965 "mtime": self.mtime,
966 "chksum": self.chksum,
967 "type": self.type,
968 "linkname": self.linkname,
969 "uname": self.uname,
970 "gname": self.gname,
971 "devmajor": self.devmajor,
972 "devminor": self.devminor
973 }
974
975 if info["type"] == DIRTYPE and not info["name"].endswith("/"):
976 info["name"] += "/"
977
978 for key in ("name", "linkname", "uname", "gname"):
979 if type(info[key]) is unicode:
980 info[key] = info[key].encode(encoding, errors)
981
982 return info
983
984 def tobuf(self, format=DEFAULT_FORMAT, encoding=ENCODING, errors="strict"):
985 """Return a tar header as a string of 512 byte blocks.
986 """
987 info = self.get_info(encoding, errors)
988
989 if format == USTAR_FORMAT:
990 return self.create_ustar_header(info)
991 elif format == GNU_FORMAT:
992 return self.create_gnu_header(info)
993 elif format == PAX_FORMAT:
994 return self.create_pax_header(info, encoding, errors)
995 else:
996 raise ValueError("invalid format")
997
998 def create_ustar_header(self, info):
999 """Return the object as a ustar header block.
1000 """
1001 info["magic"] = POSIX_MAGIC
1002
1003 if len(info["linkname"]) > LENGTH_LINK:
1004 raise ValueError("linkname is too long")
1005
1006 if len(info["name"]) > LENGTH_NAME:
1007 info["prefix"], info["name"] = self._posix_split_name(info["name"])
1008
1009 return self._create_header(info, USTAR_FORMAT)
1010
1011 def create_gnu_header(self, info):
1012 """Return the object as a GNU header block sequence.
1013 """
1014 info["magic"] = GNU_MAGIC
1015
1016 buf = ""
1017 if len(info["linkname"]) > LENGTH_LINK:
1018 buf += self._create_gnu_long_header(info["linkname"], GNUTYPE_LONGLINK)
1019
1020 if len(info["name"]) > LENGTH_NAME:
1021 buf += self._create_gnu_long_header(info["name"], GNUTYPE_LONGNAME)
1022
1023 return buf + self._create_header(info, GNU_FORMAT)
1024
1025 def create_pax_header(self, info, encoding, errors):
1026 """Return the object as a ustar header block. If it cannot be
1027 represented this way, prepend a pax extended header sequence
1028 with supplement information.
1029 """
1030 info["magic"] = POSIX_MAGIC
1031 pax_headers = self.pax_headers.copy()
1032
1033 # Test string fields for values that exceed the field length or cannot
1034 # be represented in ASCII encoding.
1035 for name, hname, length in (
1036 ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK),
1037 ("uname", "uname", 32), ("gname", "gname", 32)):
1038
1039 if hname in pax_headers:
1040 # The pax header has priority.
1041 continue
1042
1043 val = info[name].decode(encoding, errors)
1044
1045 # Try to encode the string as ASCII.
1046 try:
1047 val.encode("ascii")
1048 except UnicodeEncodeError:
1049 pax_headers[hname] = val
1050 continue
1051
1052 if len(info[name]) > length:
1053 pax_headers[hname] = val
1054
1055 # Test number fields for values that exceed the field limit or values
1056 # that like to be stored as float.
1057 for name, digits in (("uid", 8), ("gid", 8), ("size", 12), ("mtime", 12)):
1058 if name in pax_headers:
1059 # The pax header has priority. Avoid overflow.
1060 info[name] = 0
1061 continue
1062
1063 val = info[name]
1064 if not 0 <= val < 8 ** (digits - 1) or isinstance(val, float):
1065 pax_headers[name] = unicode(val)
1066 info[name] = 0
1067
1068 # Create a pax extended header if necessary.
1069 if pax_headers:
1070 buf = self._create_pax_generic_header(pax_headers)
1071 else:
1072 buf = ""
1073
1074 return buf + self._create_header(info, USTAR_FORMAT)
1075
1076 @classmethod
1077 def create_pax_global_header(cls, pax_headers):
1078 """Return the object as a pax global header block sequence.
1079 """
1080 return cls._create_pax_generic_header(pax_headers, type=XGLTYPE)
1081
1082 def _posix_split_name(self, name):
1083 """Split a name longer than 100 chars into a prefix
1084 and a name part.
1085 """
1086 prefix = name[:LENGTH_PREFIX + 1]
1087 while prefix and prefix[-1] != "/":
1088 prefix = prefix[:-1]
1089
1090 name = name[len(prefix):]
1091 prefix = prefix[:-1]
1092
1093 if not prefix or len(name) > LENGTH_NAME:
1094 raise ValueError("name is too long")
1095 return prefix, name
1096
1097 @staticmethod
1098 def _create_header(info, format):
1099 """Return a header block. info is a dictionary with file
1100 information, format must be one of the *_FORMAT constants.
1101 """
1102 parts = [
1103 stn(info.get("name", ""), 100),
1104 itn(info.get("mode", 0) & 07777, 8, format),
1105 itn(info.get("uid", 0), 8, format),
1106 itn(info.get("gid", 0), 8, format),
1107 itn(info.get("size", 0), 12, format),
1108 itn(info.get("mtime", 0), 12, format),
1109 " ", # checksum field
1110 info.get("type", REGTYPE),
1111 stn(info.get("linkname", ""), 100),
1112 stn(info.get("magic", POSIX_MAGIC), 8),
1113 stn(info.get("uname", ""), 32),
1114 stn(info.get("gname", ""), 32),
1115 itn(info.get("devmajor", 0), 8, format),
1116 itn(info.get("devminor", 0), 8, format),
1117 stn(info.get("prefix", ""), 155)
1118 ]
1119
1120 buf = struct.pack("%ds" % BLOCKSIZE, "".join(parts))
1121 chksum = calc_chksums(buf[-BLOCKSIZE:])[0]
1122 buf = buf[:-364] + "%06o\0" % chksum + buf[-357:]
1123 return buf
1124
1125 @staticmethod
1126 def _create_payload(payload):
1127 """Return the string payload filled with zero bytes
1128 up to the next 512 byte border.
1129 """
1130 blocks, remainder = divmod(len(payload), BLOCKSIZE)
1131 if remainder > 0:
1132 payload += (BLOCKSIZE - remainder) * NUL
1133 return payload
1134
1135 @classmethod
1136 def _create_gnu_long_header(cls, name, type):
1137 """Return a GNUTYPE_LONGNAME or GNUTYPE_LONGLINK sequence
1138 for name.
1139 """
1140 name += NUL
1141
1142 info = {}
1143 info["name"] = "././@LongLink"
1144 info["type"] = type
1145 info["size"] = len(name)
1146 info["magic"] = GNU_MAGIC
1147
1148 # create extended header + name blocks.
1149 return cls._create_header(info, USTAR_FORMAT) + \
1150 cls._create_payload(name)
1151
1152 @classmethod
1153 def _create_pax_generic_header(cls, pax_headers, type=XHDTYPE):
1154 """Return a POSIX.1-2001 extended or global header sequence
1155 that contains a list of keyword, value pairs. The values
1156 must be unicode objects.
1157 """
1158 records = []
1159 for keyword, value in pax_headers.iteritems():
1160 keyword = keyword.encode("utf8")
1161 value = value.encode("utf8")
1162 l = len(keyword) + len(value) + 3 # ' ' + '=' + '\n'
1163 n = p = 0
1164 while True:
1165 n = l + len(str(p))
1166 if n == p:
1167 break
1168 p = n
1169 records.append("%d %s=%s\n" % (p, keyword, value))
1170 records = "".join(records)
1171
1172 # We use a hardcoded "././@PaxHeader" name like star does
1173 # instead of the one that POSIX recommends.
1174 info = {}
1175 info["name"] = "././@PaxHeader"
1176 info["type"] = type
1177 info["size"] = len(records)
1178 info["magic"] = POSIX_MAGIC
1179
1180 # Create pax header + record blocks.
1181 return cls._create_header(info, USTAR_FORMAT) + \
1182 cls._create_payload(records)
1183
1184 @classmethod
1185 def frombuf(cls, buf):
1186 """Construct a TarInfo object from a 512 byte string buffer.
1187 """
1188 if len(buf) == 0:
1189 raise EmptyHeaderError("empty header")
1190 if len(buf) != BLOCKSIZE:
1191 raise TruncatedHeaderError("truncated header")
1192 if buf.count(NUL) == BLOCKSIZE:
1193 raise EOFHeaderError("end of file header")
1194
1195 chksum = nti(buf[148:156])
1196 if chksum not in calc_chksums(buf):
1197 raise InvalidHeaderError("bad checksum")
1198
1199 obj = cls()
1200 obj.buf = buf
1201 obj.name = nts(buf[0:100])
1202 obj.mode = nti(buf[100:108])
1203 obj.uid = nti(buf[108:116])
1204 obj.gid = nti(buf[116:124])
1205 obj.size = nti(buf[124:136])
1206 obj.mtime = nti(buf[136:148])
1207 obj.chksum = chksum
1208 obj.type = buf[156:157]
1209 obj.linkname = nts(buf[157:257])
1210 obj.uname = nts(buf[265:297])
1211 obj.gname = nts(buf[297:329])
1212 obj.devmajor = nti(buf[329:337])
1213 obj.devminor = nti(buf[337:345])
1214 prefix = nts(buf[345:500])
1215
1216 # Old V7 tar format represents a directory as a regular
1217 # file with a trailing slash.
1218 if obj.type == AREGTYPE and obj.name.endswith("/"):
1219 obj.type = DIRTYPE
1220
1221 # Remove redundant slashes from directories.
1222 if obj.isdir():
1223 obj.name = obj.name.rstrip("/")
1224
1225 # Reconstruct a ustar longname.
1226 if prefix and obj.type not in GNU_TYPES:
1227 obj.name = prefix + "/" + obj.name
1228 return obj
1229
1230 @classmethod
1231 def fromtarfile(cls, tarfile):
1232 """Return the next TarInfo object from TarFile object
1233 tarfile.
1234 """
1235 buf = tarfile.fileobj.read(BLOCKSIZE)
1236 obj = cls.frombuf(buf)
1237 obj.offset = tarfile.fileobj.tell() - BLOCKSIZE
1238 return obj._proc_member(tarfile)
1239
1240 #--------------------------------------------------------------------------
1241 # The following are methods that are called depending on the type of a
1242 # member. The entry point is _proc_member() which can be overridden in a
1243 # subclass to add custom _proc_*() methods. A _proc_*() method MUST
1244 # implement the following
1245 # operations:
1246 # 1. Set self.offset_data to the position where the data blocks begin,
1247 # if there is data that follows.
1248 # 2. Set tarfile.offset to the position where the next member's header will
1249 # begin.
1250 # 3. Return self or another valid TarInfo object.
1251 def _proc_member(self, tarfile):
1252 """Choose the right processing method depending on
1253 the type and call it.
1254 """
1255 if self.type in (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK):
1256 return self._proc_gnulong(tarfile)
1257 elif self.type == GNUTYPE_SPARSE:
1258 return self._proc_sparse(tarfile)
1259 elif self.type in (XHDTYPE, XGLTYPE, SOLARIS_XHDTYPE):
1260 return self._proc_pax(tarfile)
1261 else:
1262 return self._proc_builtin(tarfile)
1263
1264 def _proc_builtin(self, tarfile):
1265 """Process a builtin type or an unknown type which
1266 will be treated as a regular file.
1267 """
1268 self.offset_data = tarfile.fileobj.tell()
1269 offset = self.offset_data
1270 if self.isreg() or self.type not in SUPPORTED_TYPES:
1271 # Skip the following data blocks.
1272 offset += self._block(self.size)
1273 tarfile.offset = offset
1274
1275 # Patch the TarInfo object with saved global
1276 # header information.
1277 self._apply_pax_info(tarfile.pax_headers, tarfile.encoding, tarfile.errors)
1278
1279 return self
1280
1281 def _proc_gnulong(self, tarfile):
1282 """Process the blocks that hold a GNU longname
1283 or longlink member.
1284 """
1285 buf = tarfile.fileobj.read(self._block(self.size))
1286
1287 # Fetch the next header and process it.
1288 try:
1289 next = self.fromtarfile(tarfile)
1290 except HeaderError:
1291 raise SubsequentHeaderError("missing or bad subsequent header")
1292
1293 # Patch the TarInfo object from the next header with
1294 # the longname information.
1295 next.offset = self.offset
1296 if self.type == GNUTYPE_LONGNAME:
1297 next.name = nts(buf)
1298 elif self.type == GNUTYPE_LONGLINK:
1299 next.linkname = nts(buf)
1300
1301 return next
1302
1303 def _proc_sparse(self, tarfile):
1304 """Process a GNU sparse header plus extra headers.
1305 """
1306 buf = self.buf
1307 sp = _ringbuffer()
1308 pos = 386
1309 lastpos = 0L
1310 realpos = 0L
1311 # There are 4 possible sparse structs in the
1312 # first header.
1313 for i in xrange(4):
1314 try:
1315 offset = nti(buf[pos:pos + 12])
1316 numbytes = nti(buf[pos + 12:pos + 24])
1317 except ValueError:
1318 break
1319 if offset > lastpos:
1320 sp.append(_hole(lastpos, offset - lastpos))
1321 sp.append(_data(offset, numbytes, realpos))
1322 realpos += numbytes
1323 lastpos = offset + numbytes
1324 pos += 24
1325
1326 isextended = ord(buf[482])
1327 origsize = nti(buf[483:495])
1328
1329 # If the isextended flag is given,
1330 # there are extra headers to process.
1331 while isextended == 1:
1332 buf = tarfile.fileobj.read(BLOCKSIZE)
1333 pos = 0
1334 for i in xrange(21):
1335 try:
1336 offset = nti(buf[pos:pos + 12])
1337 numbytes = nti(buf[pos + 12:pos + 24])
1338 except ValueError:
1339 break
1340 if offset > lastpos:
1341 sp.append(_hole(lastpos, offset - lastpos))
1342 sp.append(_data(offset, numbytes, realpos))
1343 realpos += numbytes
1344 lastpos = offset + numbytes
1345 pos += 24
1346 isextended = ord(buf[504])
1347
1348 if lastpos < origsize:
1349 sp.append(_hole(lastpos, origsize - lastpos))
1350
1351 self.sparse = sp
1352
1353 self.offset_data = tarfile.fileobj.tell()
1354 tarfile.offset = self.offset_data + self._block(self.size)
1355 self.size = origsize
1356
1357 return self
1358
1359 def _proc_pax(self, tarfile):
1360 """Process an extended or global header as described in
1361 POSIX.1-2001.
1362 """
1363 # Read the header information.
1364 buf = tarfile.fileobj.read(self._block(self.size))
1365
1366 # A pax header stores supplemental information for either
1367 # the following file (extended) or all following files
1368 # (global).
1369 if self.type == XGLTYPE:
1370 pax_headers = tarfile.pax_headers
1371 else:
1372 pax_headers = tarfile.pax_headers.copy()
1373
1374 # Parse pax header information. A record looks like that:
1375 # "%d %s=%s\n" % (length, keyword, value). length is the size
1376 # of the complete record including the length field itself and
1377 # the newline. keyword and value are both UTF-8 encoded strings.
1378 regex = re.compile(r"(\d+) ([^=]+)=", re.U)
1379 pos = 0
1380 while True:
1381 match = regex.match(buf, pos)
1382 if not match:
1383 break
1384
1385 length, keyword = match.groups()
1386 length = int(length)
1387 value = buf[match.end(2) + 1:match.start(1) + length - 1]
1388
1389 keyword = keyword.decode("utf8")
1390 value = value.decode("utf8")
1391
1392 pax_headers[keyword] = value
1393 pos += length
1394
1395 # Fetch the next header.
1396 try:
1397 next = self.fromtarfile(tarfile)
1398 except HeaderError:
1399 raise SubsequentHeaderError("missing or bad subsequent header")
1400
1401 if self.type in (XHDTYPE, SOLARIS_XHDTYPE):
1402 # Patch the TarInfo object with the extended header info.
1403 next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors)
1404 next.offset = self.offset
1405
1406 if "size" in pax_headers:
1407 # If the extended header replaces the size field,
1408 # we need to recalculate the offset where the next
1409 # header starts.
1410 offset = next.offset_data
1411 if next.isreg() or next.type not in SUPPORTED_TYPES:
1412 offset += next._block(next.size)
1413 tarfile.offset = offset
1414
1415 return next
1416
1417 def _apply_pax_info(self, pax_headers, encoding, errors):
1418 """Replace fields with supplemental information from a previous
1419 pax extended or global header.
1420 """
1421 for keyword, value in pax_headers.iteritems():
1422 if keyword not in PAX_FIELDS:
1423 continue
1424
1425 if keyword == "path":
1426 value = value.rstrip("/")
1427
1428 if keyword in PAX_NUMBER_FIELDS:
1429 try:
1430 value = PAX_NUMBER_FIELDS[keyword](value)
1431 except ValueError:
1432 value = 0
1433 else:
1434 value = uts(value, encoding, errors)
1435
1436 setattr(self, keyword, value)
1437
1438 self.pax_headers = pax_headers.copy()
1439
1440 def _block(self, count):
1441 """Round up a byte count by BLOCKSIZE and return it,
1442 e.g. _block(834) => 1024.
1443 """
1444 blocks, remainder = divmod(count, BLOCKSIZE)
1445 if remainder:
1446 blocks += 1
1447 return blocks * BLOCKSIZE
1448
1449 def isreg(self):
1450 return self.type in REGULAR_TYPES
1451 def isfile(self):
1452 return self.isreg()
1453 def isdir(self):
1454 return self.type == DIRTYPE
1455 def issym(self):
1456 return self.type == SYMTYPE
1457 def islnk(self):
1458 return self.type == LNKTYPE
1459 def ischr(self):
1460 return self.type == CHRTYPE
1461 def isblk(self):
1462 return self.type == BLKTYPE
1463 def isfifo(self):
1464 return self.type == FIFOTYPE
1465 def issparse(self):
1466 return self.type == GNUTYPE_SPARSE
1467 def isdev(self):
1468 return self.type in (CHRTYPE, BLKTYPE, FIFOTYPE)
1469# class TarInfo
1470
1471class TarFile(object):
1472 """The TarFile Class provides an interface to tar archives.
1473 """
1474
1475 debug = 0 # May be set from 0 (no msgs) to 3 (all msgs)
1476
1477 dereference = False # If true, add content of linked file to the
1478 # tar file, else the link.
1479
1480 ignore_zeros = False # If true, skips empty or invalid blocks and
1481 # continues processing.
1482
1483 errorlevel = 1 # If 0, fatal errors only appear in debug
1484 # messages (if debug >= 0). If > 0, errors
1485 # are passed to the caller as exceptions.
1486
1487 format = DEFAULT_FORMAT # The format to use when creating an archive.
1488
1489 encoding = ENCODING # Encoding for 8-bit character strings.
1490
1491 errors = None # Error handler for unicode conversion.
1492
1493 tarinfo = TarInfo # The default TarInfo class to use.
1494
1495 fileobject = ExFileObject # The default ExFileObject class to use.
1496
1497 def __init__(self, name=None, mode="r", fileobj=None, format=None,
1498 tarinfo=None, dereference=None, ignore_zeros=None, encoding=None,
1499 errors=None, pax_headers=None, debug=None, errorlevel=None):
1500 """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to
1501 read from an existing archive, 'a' to append data to an existing
1502 file or 'w' to create a new file overwriting an existing one. `mode'
1503 defaults to 'r'.
1504 If `fileobj' is given, it is used for reading or writing data. If it
1505 can be determined, `mode' is overridden by `fileobj's mode.
1506 `fileobj' is not closed, when TarFile is closed.
1507 """
1508 if len(mode) > 1 or mode not in "raw":
1509 raise ValueError("mode must be 'r', 'a' or 'w'")
1510 self.mode = mode
1511 self._mode = {"r": "rb", "a": "r+b", "w": "wb"}[mode]
1512
1513 if not fileobj:
1514 if self.mode == "a" and not os.path.exists(name):
1515 # Create nonexistent files in append mode.
1516 self.mode = "w"
1517 self._mode = "wb"
1518 fileobj = bltn_open(name, self._mode)
1519 self._extfileobj = False
1520 else:
1521 if name is None and hasattr(fileobj, "name"):
1522 name = fileobj.name
1523 if hasattr(fileobj, "mode"):
1524 self._mode = fileobj.mode
1525 self._extfileobj = True
1526 if name:
1527 self.name = os.path.abspath(name)
1528 else:
1529 self.name = None
1530 self.fileobj = fileobj
1531
1532 # Init attributes.
1533 if format is not None:
1534 self.format = format
1535 if tarinfo is not None:
1536 self.tarinfo = tarinfo
1537 if dereference is not None:
1538 self.dereference = dereference
1539 if ignore_zeros is not None:
1540 self.ignore_zeros = ignore_zeros
1541 if encoding is not None:
1542 self.encoding = encoding
1543
1544 if errors is not None:
1545 self.errors = errors
1546 elif mode == "r":
1547 self.errors = "utf-8"
1548 else:
1549 self.errors = "strict"
1550
1551 if pax_headers is not None and self.format == PAX_FORMAT:
1552 self.pax_headers = pax_headers
1553 else:
1554 self.pax_headers = {}
1555
1556 if debug is not None:
1557 self.debug = debug
1558 if errorlevel is not None:
1559 self.errorlevel = errorlevel
1560
1561 # Init datastructures.
1562 self.closed = False
1563 self.members = [] # list of members as TarInfo objects
1564 self._loaded = False # flag if all members have been read
1565 self.offset = self.fileobj.tell()
1566 # current position in the archive file
1567 self.inodes = {} # dictionary caching the inodes of
1568 # archive members already added
1569
1570 try:
1571 if self.mode == "r":
1572 self.firstmember = None
1573 self.firstmember = self.next()
1574
1575 if self.mode == "a":
1576 # Move to the end of the archive,
1577 # before the first empty block.
1578 while True:
1579 self.fileobj.seek(self.offset)
1580 try:
1581 tarinfo = self.tarinfo.fromtarfile(self)
1582 self.members.append(tarinfo)
1583 except EOFHeaderError:
1584 self.fileobj.seek(self.offset)
1585 break
1586 except HeaderError, e:
1587 raise ReadError(str(e))
1588
1589 if self.mode in "aw":
1590 self._loaded = True
1591
1592 if self.pax_headers:
1593 buf = self.tarinfo.create_pax_global_header(self.pax_headers.copy())
1594 self.fileobj.write(buf)
1595 self.offset += len(buf)
1596 except:
1597 if not self._extfileobj:
1598 self.fileobj.close()
1599 self.closed = True
1600 raise
1601
1602 def _getposix(self):
1603 return self.format == USTAR_FORMAT
1604 def _setposix(self, value):
1605 import warnings
1606 warnings.warn("use the format attribute instead", DeprecationWarning,
1607 2)
1608 if value:
1609 self.format = USTAR_FORMAT
1610 else:
1611 self.format = GNU_FORMAT
1612 posix = property(_getposix, _setposix)
1613
1614 #--------------------------------------------------------------------------
1615 # Below are the classmethods which act as alternate constructors to the
1616 # TarFile class. The open() method is the only one that is needed for
1617 # public use; it is the "super"-constructor and is able to select an
1618 # adequate "sub"-constructor for a particular compression using the mapping
1619 # from OPEN_METH.
1620 #
1621 # This concept allows one to subclass TarFile without losing the comfort of
1622 # the super-constructor. A sub-constructor is registered and made available
1623 # by adding it to the mapping in OPEN_METH.
1624
1625 @classmethod
1626 def open(cls, name=None, mode="r", fileobj=None, bufsize=RECORDSIZE, **kwargs):
1627 """Open a tar archive for reading, writing or appending. Return
1628 an appropriate TarFile class.
1629
1630 mode:
1631 'r' or 'r:*' open for reading with transparent compression
1632 'r:' open for reading exclusively uncompressed
1633 'r:gz' open for reading with gzip compression
1634 'r:bz2' open for reading with bzip2 compression
1635 'a' or 'a:' open for appending, creating the file if necessary
1636 'w' or 'w:' open for writing without compression
1637 'w:gz' open for writing with gzip compression
1638 'w:bz2' open for writing with bzip2 compression
1639
1640 'r|*' open a stream of tar blocks with transparent compression
1641 'r|' open an uncompressed stream of tar blocks for reading
1642 'r|gz' open a gzip compressed stream of tar blocks
1643 'r|bz2' open a bzip2 compressed stream of tar blocks
1644 'w|' open an uncompressed stream for writing
1645 'w|gz' open a gzip compressed stream for writing
1646 'w|bz2' open a bzip2 compressed stream for writing
1647 """
1648
1649 if not name and not fileobj:
1650 raise ValueError("nothing to open")
1651
1652 if mode in ("r", "r:*"):
1653 # Find out which *open() is appropriate for opening the file.
1654 for comptype in cls.OPEN_METH:
1655 func = getattr(cls, cls.OPEN_METH[comptype])
1656 if fileobj is not None:
1657 saved_pos = fileobj.tell()
1658 try:
1659 return func(name, "r", fileobj, **kwargs)
1660 except (ReadError, CompressionError), e:
1661 if fileobj is not None:
1662 fileobj.seek(saved_pos)
1663 continue
1664 raise ReadError("file could not be opened successfully")
1665
1666 elif ":" in mode:
1667 filemode, comptype = mode.split(":", 1)
1668 filemode = filemode or "r"
1669 comptype = comptype or "tar"
1670
1671 # Select the *open() function according to
1672 # given compression.
1673 if comptype in cls.OPEN_METH:
1674 func = getattr(cls, cls.OPEN_METH[comptype])
1675 else:
1676 raise CompressionError("unknown compression type %r" % comptype)
1677 return func(name, filemode, fileobj, **kwargs)
1678
1679 elif "|" in mode:
1680 filemode, comptype = mode.split("|", 1)
1681 filemode = filemode or "r"
1682 comptype = comptype or "tar"
1683
1684 if filemode not in "rw":
1685 raise ValueError("mode must be 'r' or 'w'")
1686
1687 t = cls(name, filemode,
1688 _Stream(name, filemode, comptype, fileobj, bufsize),
1689 **kwargs)
1690 t._extfileobj = False
1691 return t
1692
1693 elif mode in "aw":
1694 return cls.taropen(name, mode, fileobj, **kwargs)
1695
1696 raise ValueError("undiscernible mode")
1697
1698 @classmethod
1699 def taropen(cls, name, mode="r", fileobj=None, **kwargs):
1700 """Open uncompressed tar archive name for reading or writing.
1701 """
1702 if len(mode) > 1 or mode not in "raw":
1703 raise ValueError("mode must be 'r', 'a' or 'w'")
1704 return cls(name, mode, fileobj, **kwargs)
1705
1706 @classmethod
1707 def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs):
1708 """Open gzip compressed tar archive name for reading or writing.
1709 Appending is not allowed.
1710 """
1711 if len(mode) > 1 or mode not in "rw":
1712 raise ValueError("mode must be 'r' or 'w'")
1713
1714 try:
1715 import gzip
1716 gzip.GzipFile
1717 except (ImportError, AttributeError):
1718 raise CompressionError("gzip module is not available")
1719
1720 if fileobj is None:
1721 fileobj = bltn_open(name, mode + "b")
1722
1723 try:
1724 t = cls.taropen(name, mode,
1725 gzip.GzipFile(name, mode, compresslevel, fileobj),
1726 **kwargs)
1727 except IOError:
1728 raise ReadError("not a gzip file")
1729 t._extfileobj = False
1730 return t
1731
1732 @classmethod
1733 def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs):
1734 """Open bzip2 compressed tar archive name for reading or writing.
1735 Appending is not allowed.
1736 """
1737 if len(mode) > 1 or mode not in "rw":
1738 raise ValueError("mode must be 'r' or 'w'.")
1739
1740 try:
1741 import bz2
1742 except ImportError:
1743 raise CompressionError("bz2 module is not available")
1744
1745 if fileobj is not None:
1746 fileobj = _BZ2Proxy(fileobj, mode)
1747 else:
1748 fileobj = bz2.BZ2File(name, mode, compresslevel=compresslevel)
1749
1750 try:
1751 t = cls.taropen(name, mode, fileobj, **kwargs)
1752 except (IOError, EOFError):
1753 raise ReadError("not a bzip2 file")
1754 t._extfileobj = False
1755 return t
1756
1757 # All *open() methods are registered here.
1758 OPEN_METH = {
1759 "tar": "taropen", # uncompressed tar
1760 "gz": "gzopen", # gzip compressed tar
1761 "bz2": "bz2open" # bzip2 compressed tar
1762 }
1763
1764 #--------------------------------------------------------------------------
1765 # The public methods which TarFile provides:
1766
1767 def close(self):
1768 """Close the TarFile. In write-mode, two finishing zero blocks are
1769 appended to the archive.
1770 """
1771 if self.closed:
1772 return
1773
1774 if self.mode in "aw":
1775 self.fileobj.write(NUL * (BLOCKSIZE * 2))
1776 self.offset += (BLOCKSIZE * 2)
1777 # fill up the end with zero-blocks
1778 # (like option -b20 for tar does)
1779 blocks, remainder = divmod(self.offset, RECORDSIZE)
1780 if remainder > 0:
1781 self.fileobj.write(NUL * (RECORDSIZE - remainder))
1782
1783 if not self._extfileobj:
1784 self.fileobj.close()
1785 self.closed = True
1786
1787 def getmember(self, name):
1788 """Return a TarInfo object for member `name'. If `name' can not be
1789 found in the archive, KeyError is raised. If a member occurs more
1790 than once in the archive, its last occurrence is assumed to be the
1791 most up-to-date version.
1792 """
1793 tarinfo = self._getmember(name)
1794 if tarinfo is None:
1795 raise KeyError("filename %r not found" % name)
1796 return tarinfo
1797
1798 def getmembers(self):
1799 """Return the members of the archive as a list of TarInfo objects. The
1800 list has the same order as the members in the archive.
1801 """
1802 self._check()
1803 if not self._loaded: # if we want to obtain a list of
1804 self._load() # all members, we first have to
1805 # scan the whole archive.
1806 return self.members
1807
1808 def getnames(self):
1809 """Return the members of the archive as a list of their names. It has
1810 the same order as the list returned by getmembers().
1811 """
1812 return [tarinfo.name for tarinfo in self.getmembers()]
1813
1814 def gettarinfo(self, name=None, arcname=None, fileobj=None):
1815 """Create a TarInfo object for either the file `name' or the file
1816 object `fileobj' (using os.fstat on its file descriptor). You can
1817 modify some of the TarInfo's attributes before you add it using
1818 addfile(). If given, `arcname' specifies an alternative name for the
1819 file in the archive.
1820 """
1821 self._check("aw")
1822
1823 # When fileobj is given, replace name by
1824 # fileobj's real name.
1825 if fileobj is not None:
1826 name = fileobj.name
1827
1828 # Building the name of the member in the archive.
1829 # Backward slashes are converted to forward slashes,
1830 # Absolute paths are turned to relative paths.
1831 if arcname is None:
1832 arcname = name
1833 drv, arcname = os.path.splitdrive(arcname)
1834 arcname = arcname.replace(os.sep, "/")
1835 arcname = arcname.lstrip("/")
1836
1837 # Now, fill the TarInfo object with
1838 # information specific for the file.
1839 tarinfo = self.tarinfo()
1840 tarinfo.tarfile = self
1841
1842 # Use os.stat or os.lstat, depending on platform
1843 # and if symlinks shall be resolved.
1844 if fileobj is None:
1845 if hasattr(os, "lstat") and not self.dereference:
1846 statres = os.lstat(name)
1847 else:
1848 statres = os.stat(name)
1849 else:
1850 statres = os.fstat(fileobj.fileno())
1851 linkname = ""
1852
1853 stmd = statres.st_mode
1854 if stat.S_ISREG(stmd):
1855 inode = (statres.st_ino, statres.st_dev)
1856 if not self.dereference and statres.st_nlink > 1 and \
1857 inode in self.inodes and arcname != self.inodes[inode]:
1858 # Is it a hardlink to an already
1859 # archived file?
1860 type = LNKTYPE
1861 linkname = self.inodes[inode]
1862 else:
1863 # The inode is added only if its valid.
1864 # For win32 it is always 0.
1865 type = REGTYPE
1866 if inode[0]:
1867 self.inodes[inode] = arcname
1868 elif stat.S_ISDIR(stmd):
1869 type = DIRTYPE
1870 elif stat.S_ISFIFO(stmd):
1871 type = FIFOTYPE
1872 elif stat.S_ISLNK(stmd):
1873 type = SYMTYPE
1874 linkname = os.readlink(name)
1875 elif stat.S_ISCHR(stmd):
1876 type = CHRTYPE
1877 elif stat.S_ISBLK(stmd):
1878 type = BLKTYPE
1879 else:
1880 return None
1881
1882 # Fill the TarInfo object with all
1883 # information we can get.
1884 tarinfo.name = arcname
1885 tarinfo.mode = stmd
1886 tarinfo.uid = statres.st_uid
1887 tarinfo.gid = statres.st_gid
1888 if type == REGTYPE:
1889 tarinfo.size = statres.st_size
1890 else:
1891 tarinfo.size = 0L
1892 tarinfo.mtime = statres.st_mtime
1893 tarinfo.type = type
1894 tarinfo.linkname = linkname
1895 if pwd:
1896 try:
1897 tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0]
1898 except KeyError:
1899 pass
1900 if grp:
1901 try:
1902 tarinfo.gname = grp.getgrgid(tarinfo.gid)[0]
1903 except KeyError:
1904 pass
1905
1906 if type in (CHRTYPE, BLKTYPE):
1907 if hasattr(os, "major") and hasattr(os, "minor"):
1908 tarinfo.devmajor = os.major(statres.st_rdev)
1909 tarinfo.devminor = os.minor(statres.st_rdev)
1910 return tarinfo
1911
1912 def list(self, verbose=True):
1913 """Print a table of contents to sys.stdout. If `verbose' is False, only
1914 the names of the members are printed. If it is True, an `ls -l'-like
1915 output is produced.
1916 """
1917 self._check()
1918
1919 for tarinfo in self:
1920 if verbose:
1921 print filemode(tarinfo.mode),
1922 print "%s/%s" % (tarinfo.uname or tarinfo.uid,
1923 tarinfo.gname or tarinfo.gid),
1924 if tarinfo.ischr() or tarinfo.isblk():
1925 print "%10s" % ("%d,%d" \
1926 % (tarinfo.devmajor, tarinfo.devminor)),
1927 else:
1928 print "%10d" % tarinfo.size,
1929 print "%d-%02d-%02d %02d:%02d:%02d" \
1930 % time.localtime(tarinfo.mtime)[:6],
1931
1932 if tarinfo.isdir():
1933 print tarinfo.name + "/",
1934 else:
1935 print tarinfo.name,
1936
1937 if verbose:
1938 if tarinfo.issym():
1939 print "->", tarinfo.linkname,
1940 if tarinfo.islnk():
1941 print "link to", tarinfo.linkname,
1942 print
1943
1944 def add(self, name, arcname=None, recursive=True, exclude=None, filter=None):
1945 """Add the file `name' to the archive. `name' may be any type of file
1946 (directory, fifo, symbolic link, etc.). If given, `arcname'
1947 specifies an alternative name for the file in the archive.
1948 Directories are added recursively by default. This can be avoided by
1949 setting `recursive' to False. `exclude' is a function that should
1950 return True for each filename to be excluded. `filter' is a function
1951 that expects a TarInfo object argument and returns the changed
1952 TarInfo object, if it returns None the TarInfo object will be
1953 excluded from the archive.
1954 """
1955 self._check("aw")
1956
1957 if arcname is None:
1958 arcname = name
1959
1960 # Exclude pathnames.
1961 if exclude is not None:
1962 import warnings
1963 warnings.warn("use the filter argument instead",
1964 DeprecationWarning, 2)
1965 if exclude(name):
1966 self._dbg(2, "tarfile: Excluded %r" % name)
1967 return
1968
1969 # Skip if somebody tries to archive the archive...
1970 if self.name is not None and os.path.abspath(name) == self.name:
1971 self._dbg(2, "tarfile: Skipped %r" % name)
1972 return
1973
1974 self._dbg(1, name)
1975
1976 # Create a TarInfo object from the file.
1977 tarinfo = self.gettarinfo(name, arcname)
1978
1979 if tarinfo is None:
1980 self._dbg(1, "tarfile: Unsupported type %r" % name)
1981 return
1982
1983 # Change or exclude the TarInfo object.
1984 if filter is not None:
1985 tarinfo = filter(tarinfo)
1986 if tarinfo is None:
1987 self._dbg(2, "tarfile: Excluded %r" % name)
1988 return
1989
1990 # Append the tar header and data to the archive.
1991 if tarinfo.isreg():
1992 f = bltn_open(name, "rb")
1993 self.addfile(tarinfo, f)
1994 f.close()
1995
1996 elif tarinfo.isdir():
1997 self.addfile(tarinfo)
1998 if recursive:
1999 for f in os.listdir(name):
2000 self.add(os.path.join(name, f), os.path.join(arcname, f),
2001 recursive, exclude, filter)
2002
2003 else:
2004 self.addfile(tarinfo)
2005
2006 def addfile(self, tarinfo, fileobj=None):
2007 """Add the TarInfo object `tarinfo' to the archive. If `fileobj' is
2008 given, tarinfo.size bytes are read from it and added to the archive.
2009 You can create TarInfo objects using gettarinfo().
2010 On Windows platforms, `fileobj' should always be opened with mode
2011 'rb' to avoid irritation about the file size.
2012 """
2013 self._check("aw")
2014
2015 tarinfo = copy.copy(tarinfo)
2016
2017 buf = tarinfo.tobuf(self.format, self.encoding, self.errors)
2018 self.fileobj.write(buf)
2019 self.offset += len(buf)
2020
2021 # If there's data to follow, append it.
2022 if fileobj is not None:
2023 copyfileobj(fileobj, self.fileobj, tarinfo.size)
2024 blocks, remainder = divmod(tarinfo.size, BLOCKSIZE)
2025 if remainder > 0:
2026 self.fileobj.write(NUL * (BLOCKSIZE - remainder))
2027 blocks += 1
2028 self.offset += blocks * BLOCKSIZE
2029
2030 self.members.append(tarinfo)
2031
2032 def extractall(self, path=".", members=None):
2033 """Extract all members from the archive to the current working
2034 directory and set owner, modification time and permissions on
2035 directories afterwards. `path' specifies a different directory
2036 to extract to. `members' is optional and must be a subset of the
2037 list returned by getmembers().
2038 """
2039 directories = []
2040
2041 if members is None:
2042 members = self
2043
2044 for tarinfo in members:
2045 if tarinfo.isdir():
2046 # Extract directories with a safe mode.
2047 directories.append(tarinfo)
2048 tarinfo = copy.copy(tarinfo)
2049 tarinfo.mode = 0700
2050 self.extract(tarinfo, path)
2051
2052 # Reverse sort directories.
2053 directories.sort(key=operator.attrgetter('name'))
2054 directories.reverse()
2055
2056 # Set correct owner, mtime and filemode on directories.
2057 for tarinfo in directories:
2058 dirpath = os.path.join(path, tarinfo.name)
2059 try:
2060 self.chown(tarinfo, dirpath)
2061 self.utime(tarinfo, dirpath)
2062 self.chmod(tarinfo, dirpath)
2063 except ExtractError, e:
2064 if self.errorlevel > 1:
2065 raise
2066 else:
2067 self._dbg(1, "tarfile: %s" % e)
2068
2069 def extract(self, member, path=""):
2070 """Extract a member from the archive to the current working directory,
2071 using its full name. Its file information is extracted as accurately
2072 as possible. `member' may be a filename or a TarInfo object. You can
2073 specify a different directory using `path'.
2074 """
2075 self._check("r")
2076
2077 if isinstance(member, basestring):
2078 tarinfo = self.getmember(member)
2079 else:
2080 tarinfo = member
2081
2082 # Prepare the link target for makelink().
2083 if tarinfo.islnk():
2084 tarinfo._link_target = os.path.join(path, tarinfo.linkname)
2085
2086 try:
2087 self._extract_member(tarinfo, os.path.join(path, tarinfo.name))
2088 except EnvironmentError, e:
2089 if self.errorlevel > 0:
2090 raise
2091 else:
2092 if e.filename is None:
2093 self._dbg(1, "tarfile: %s" % e.strerror)
2094 else:
2095 self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename))
2096 except ExtractError, e:
2097 if self.errorlevel > 1:
2098 raise
2099 else:
2100 self._dbg(1, "tarfile: %s" % e)
2101
2102 def extractfile(self, member):
2103 """Extract a member from the archive as a file object. `member' may be
2104 a filename or a TarInfo object. If `member' is a regular file, a
2105 file-like object is returned. If `member' is a link, a file-like
2106 object is constructed from the link's target. If `member' is none of
2107 the above, None is returned.
2108 The file-like object is read-only and provides the following
2109 methods: read(), readline(), readlines(), seek() and tell()
2110 """
2111 self._check("r")
2112
2113 if isinstance(member, basestring):
2114 tarinfo = self.getmember(member)
2115 else:
2116 tarinfo = member
2117
2118 if tarinfo.isreg():
2119 return self.fileobject(self, tarinfo)
2120
2121 elif tarinfo.type not in SUPPORTED_TYPES:
2122 # If a member's type is unknown, it is treated as a
2123 # regular file.
2124 return self.fileobject(self, tarinfo)
2125
2126 elif tarinfo.islnk() or tarinfo.issym():
2127 if isinstance(self.fileobj, _Stream):
2128 # A small but ugly workaround for the case that someone tries
2129 # to extract a (sym)link as a file-object from a non-seekable
2130 # stream of tar blocks.
2131 raise StreamError("cannot extract (sym)link as file object")
2132 else:
2133 # A (sym)link's file object is its target's file object.
2134 return self.extractfile(self._find_link_target(tarinfo))
2135 else:
2136 # If there's no data associated with the member (directory, chrdev,
2137 # blkdev, etc.), return None instead of a file object.
2138 return None
2139
2140 def _extract_member(self, tarinfo, targetpath):
2141 """Extract the TarInfo object tarinfo to a physical
2142 file called targetpath.
2143 """
2144 # Fetch the TarInfo object for the given name
2145 # and build the destination pathname, replacing
2146 # forward slashes to platform specific separators.
2147 targetpath = targetpath.rstrip("/")
2148 targetpath = targetpath.replace("/", os.sep)
2149
2150 # Create all upper directories.
2151 upperdirs = os.path.dirname(targetpath)
2152 if upperdirs and not os.path.exists(upperdirs):
2153 # Create directories that are not part of the archive with
2154 # default permissions.
2155 os.makedirs(upperdirs)
2156
2157 if tarinfo.islnk() or tarinfo.issym():
2158 self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname))
2159 else:
2160 self._dbg(1, tarinfo.name)
2161
2162 if tarinfo.isreg():
2163 self.makefile(tarinfo, targetpath)
2164 elif tarinfo.isdir():
2165 self.makedir(tarinfo, targetpath)
2166 elif tarinfo.isfifo():
2167 self.makefifo(tarinfo, targetpath)
2168 elif tarinfo.ischr() or tarinfo.isblk():
2169 self.makedev(tarinfo, targetpath)
2170 elif tarinfo.islnk() or tarinfo.issym():
2171 self.makelink(tarinfo, targetpath)
2172 elif tarinfo.type not in SUPPORTED_TYPES:
2173 self.makeunknown(tarinfo, targetpath)
2174 else:
2175 self.makefile(tarinfo, targetpath)
2176
2177 self.chown(tarinfo, targetpath)
2178 if not tarinfo.issym():
2179 self.chmod(tarinfo, targetpath)
2180 self.utime(tarinfo, targetpath)
2181
2182 #--------------------------------------------------------------------------
2183 # Below are the different file methods. They are called via
2184 # _extract_member() when extract() is called. They can be replaced in a
2185 # subclass to implement other functionality.
2186
2187 def makedir(self, tarinfo, targetpath):
2188 """Make a directory called targetpath.
2189 """
2190 try:
2191 # Use a safe mode for the directory, the real mode is set
2192 # later in _extract_member().
2193 os.mkdir(targetpath, 0700)
2194 except EnvironmentError, e:
2195 if e.errno != errno.EEXIST:
2196 raise
2197
2198 def makefile(self, tarinfo, targetpath):
2199 """Make a file called targetpath.
2200 """
2201 source = self.extractfile(tarinfo)
2202 target = bltn_open(targetpath, "wb")
2203 copyfileobj(source, target)
2204 source.close()
2205 target.close()
2206
2207 def makeunknown(self, tarinfo, targetpath):
2208 """Make a file from a TarInfo object with an unknown type
2209 at targetpath.
2210 """
2211 self.makefile(tarinfo, targetpath)
2212 self._dbg(1, "tarfile: Unknown file type %r, " \
2213 "extracted as regular file." % tarinfo.type)
2214
2215 def makefifo(self, tarinfo, targetpath):
2216 """Make a fifo called targetpath.
2217 """
2218 if hasattr(os, "mkfifo"):
2219 os.mkfifo(targetpath)
2220 else:
2221 raise ExtractError("fifo not supported by system")
2222
2223 def makedev(self, tarinfo, targetpath):
2224 """Make a character or block device called targetpath.
2225 """
2226 if not hasattr(os, "mknod") or not hasattr(os, "makedev"):
2227 raise ExtractError("special devices not supported by system")
2228
2229 mode = tarinfo.mode
2230 if tarinfo.isblk():
2231 mode |= stat.S_IFBLK
2232 else:
2233 mode |= stat.S_IFCHR
2234
2235 os.mknod(targetpath, mode,
2236 os.makedev(tarinfo.devmajor, tarinfo.devminor))
2237
2238 def makelink(self, tarinfo, targetpath):
2239 """Make a (symbolic) link called targetpath. If it cannot be created
2240 (platform limitation), we try to make a copy of the referenced file
2241 instead of a link.
2242 """
2243 if hasattr(os, "symlink") and hasattr(os, "link"):
2244 # For systems that support symbolic and hard links.
2245 if tarinfo.issym():
2246 os.symlink(tarinfo.linkname, targetpath)
2247 else:
2248 # See extract().
2249 if os.path.exists(tarinfo._link_target):
2250 os.link(tarinfo._link_target, targetpath)
2251 else:
2252 self._extract_member(self._find_link_target(tarinfo), targetpath)
2253 else:
2254 try:
2255 self._extract_member(self._find_link_target(tarinfo), targetpath)
2256 except KeyError:
2257 raise ExtractError("unable to resolve link inside archive")
2258
2259 def chown(self, tarinfo, targetpath):
2260 """Set owner of targetpath according to tarinfo.
2261 """
2262 if pwd and hasattr(os, "geteuid") and os.geteuid() == 0:
2263 # We have to be root to do so.
2264 try:
2265 g = grp.getgrnam(tarinfo.gname)[2]
2266 except KeyError:
2267 try:
2268 g = grp.getgrgid(tarinfo.gid)[2]
2269 except KeyError:
2270 g = os.getgid()
2271 try:
2272 u = pwd.getpwnam(tarinfo.uname)[2]
2273 except KeyError:
2274 try:
2275 u = pwd.getpwuid(tarinfo.uid)[2]
2276 except KeyError:
2277 u = os.getuid()
2278 try:
2279 if tarinfo.issym() and hasattr(os, "lchown"):
2280 os.lchown(targetpath, u, g)
2281 else:
2282 if sys.platform != "os2emx":
2283 os.chown(targetpath, u, g)
2284 except EnvironmentError, e:
2285 raise ExtractError("could not change owner to %d:%d" % (u, g))
2286
2287 def chmod(self, tarinfo, targetpath):
2288 """Set file permissions of targetpath according to tarinfo.
2289 """
2290 if hasattr(os, 'chmod'):
2291 try:
2292 os.chmod(targetpath, tarinfo.mode)
2293 except EnvironmentError, e:
2294 raise ExtractError("could not change mode")
2295
2296 def utime(self, tarinfo, targetpath):
2297 """Set modification time of targetpath according to tarinfo.
2298 """
2299 if not hasattr(os, 'utime'):
2300 return
2301 try:
2302 os.utime(targetpath, (tarinfo.mtime, tarinfo.mtime))
2303 except EnvironmentError, e:
2304 raise ExtractError("could not change modification time")
2305
2306 #--------------------------------------------------------------------------
2307 def next(self):
2308 """Return the next member of the archive as a TarInfo object, when
2309 TarFile is opened for reading. Return None if there is no more
2310 available.
2311 """
2312 self._check("ra")
2313 if self.firstmember is not None:
2314 m = self.firstmember
2315 self.firstmember = None
2316 return m
2317
2318 # Read the next block.
2319 self.fileobj.seek(self.offset)
2320 tarinfo = None
2321 while True:
2322 try:
2323 tarinfo = self.tarinfo.fromtarfile(self)
2324 except EOFHeaderError, e:
2325 if self.ignore_zeros:
2326 self._dbg(2, "0x%X: %s" % (self.offset, e))
2327 self.offset += BLOCKSIZE
2328 continue
2329 except InvalidHeaderError, e:
2330 if self.ignore_zeros:
2331 self._dbg(2, "0x%X: %s" % (self.offset, e))
2332 self.offset += BLOCKSIZE
2333 continue
2334 elif self.offset == 0:
2335 raise ReadError(str(e))
2336 except EmptyHeaderError:
2337 if self.offset == 0:
2338 raise ReadError("empty file")
2339 except TruncatedHeaderError, e:
2340 if self.offset == 0:
2341 raise ReadError(str(e))
2342 except SubsequentHeaderError, e:
2343 raise ReadError(str(e))
2344 break
2345
2346 if tarinfo is not None:
2347 self.members.append(tarinfo)
2348 else:
2349 self._loaded = True
2350
2351 return tarinfo
2352
2353 #--------------------------------------------------------------------------
2354 # Little helper methods:
2355
2356 def _getmember(self, name, tarinfo=None, normalize=False):
2357 """Find an archive member by name from bottom to top.
2358 If tarinfo is given, it is used as the starting point.
2359 """
2360 # Ensure that all members have been loaded.
2361 members = self.getmembers()
2362
2363 # Limit the member search list up to tarinfo.
2364 if tarinfo is not None:
2365 members = members[:members.index(tarinfo)]
2366
2367 if normalize:
2368 name = os.path.normpath(name)
2369
2370 for member in reversed(members):
2371 if normalize:
2372 member_name = os.path.normpath(member.name)
2373 else:
2374 member_name = member.name
2375
2376 if name == member_name:
2377 return member
2378
2379 def _load(self):
2380 """Read through the entire archive file and look for readable
2381 members.
2382 """
2383 while True:
2384 tarinfo = self.next()
2385 if tarinfo is None:
2386 break
2387 self._loaded = True
2388
2389 def _check(self, mode=None):
2390 """Check if TarFile is still open, and if the operation's mode
2391 corresponds to TarFile's mode.
2392 """
2393 if self.closed:
2394 raise IOError("%s is closed" % self.__class__.__name__)
2395 if mode is not None and self.mode not in mode:
2396 raise IOError("bad operation for mode %r" % self.mode)
2397
2398 def _find_link_target(self, tarinfo):
2399 """Find the target member of a symlink or hardlink member in the
2400 archive.
2401 """
2402 if tarinfo.issym():
2403 # Always search the entire archive.
2404 linkname = os.path.dirname(tarinfo.name) + "/" + tarinfo.linkname
2405 limit = None
2406 else:
2407 # Search the archive before the link, because a hard link is
2408 # just a reference to an already archived file.
2409 linkname = tarinfo.linkname
2410 limit = tarinfo
2411
2412 member = self._getmember(linkname, tarinfo=limit, normalize=True)
2413 if member is None:
2414 raise KeyError("linkname %r not found" % linkname)
2415 return member
2416
2417 def __iter__(self):
2418 """Provide an iterator object.
2419 """
2420 if self._loaded:
2421 return iter(self.members)
2422 else:
2423 return TarIter(self)
2424
2425 def _dbg(self, level, msg):
2426 """Write debugging output to sys.stderr.
2427 """
2428 if level <= self.debug:
2429 print >> sys.stderr, msg
2430
2431 def __enter__(self):
2432 self._check()
2433 return self
2434
2435 def __exit__(self, type, value, traceback):
2436 if type is None:
2437 self.close()
2438 else:
2439 # An exception occurred. We must not call close() because
2440 # it would try to write end-of-archive blocks and padding.
2441 if not self._extfileobj:
2442 self.fileobj.close()
2443 self.closed = True
2444# class TarFile
2445
2446class TarIter:
2447 """Iterator Class.
2448
2449 for tarinfo in TarFile(...):
2450 suite...
2451 """
2452
2453 def __init__(self, tarfile):
2454 """Construct a TarIter object.
2455 """
2456 self.tarfile = tarfile
2457 self.index = 0
2458 def __iter__(self):
2459 """Return iterator object.
2460 """
2461 return self
2462 def next(self):
2463 """Return the next item using TarFile's next() method.
2464 When all members have been read, set TarFile as _loaded.
2465 """
2466 # Fix for SF #1100429: Under rare circumstances it can
2467 # happen that getmembers() is called during iteration,
2468 # which will cause TarIter to stop prematurely.
2469 if not self.tarfile._loaded:
2470 tarinfo = self.tarfile.next()
2471 if not tarinfo:
2472 self.tarfile._loaded = True
2473 raise StopIteration
2474 else:
2475 try:
2476 tarinfo = self.tarfile.members[self.index]
2477 except IndexError:
2478 raise StopIteration
2479 self.index += 1
2480 return tarinfo
2481
2482# Helper classes for sparse file support
2483class _section:
2484 """Base class for _data and _hole.
2485 """
2486 def __init__(self, offset, size):
2487 self.offset = offset
2488 self.size = size
2489 def __contains__(self, offset):
2490 return self.offset <= offset < self.offset + self.size
2491
2492class _data(_section):
2493 """Represent a data section in a sparse file.
2494 """
2495 def __init__(self, offset, size, realpos):
2496 _section.__init__(self, offset, size)
2497 self.realpos = realpos
2498
2499class _hole(_section):
2500 """Represent a hole section in a sparse file.
2501 """
2502 pass
2503
2504class _ringbuffer(list):
2505 """Ringbuffer class which increases performance
2506 over a regular list.
2507 """
2508 def __init__(self):
2509 self.idx = 0
2510 def find(self, offset):
2511 idx = self.idx
2512 while True:
2513 item = self[idx]
2514 if offset in item:
2515 break
2516 idx += 1
2517 if idx == len(self):
2518 idx = 0
2519 if idx == self.idx:
2520 # End of File
2521 return None
2522 self.idx = idx
2523 return item
2524
2525#---------------------------------------------
2526# zipfile compatible TarFile class
2527#---------------------------------------------
2528TAR_PLAIN = 0 # zipfile.ZIP_STORED
2529TAR_GZIPPED = 8 # zipfile.ZIP_DEFLATED
2530class TarFileCompat:
2531 """TarFile class compatible with standard module zipfile's
2532 ZipFile class.
2533 """
2534 def __init__(self, file, mode="r", compression=TAR_PLAIN):
2535 from warnings import warnpy3k
2536 warnpy3k("the TarFileCompat class has been removed in Python 3.0",
2537 stacklevel=2)
2538 if compression == TAR_PLAIN:
2539 self.tarfile = TarFile.taropen(file, mode)
2540 elif compression == TAR_GZIPPED:
2541 self.tarfile = TarFile.gzopen(file, mode)
2542 else:
2543 raise ValueError("unknown compression constant")
2544 if mode[0:1] == "r":
2545 members = self.tarfile.getmembers()
2546 for m in members:
2547 m.filename = m.name
2548 m.file_size = m.size
2549 m.date_time = time.gmtime(m.mtime)[:6]
2550 def namelist(self):
2551 return map(lambda m: m.name, self.infolist())
2552 def infolist(self):
2553 return filter(lambda m: m.type in REGULAR_TYPES,
2554 self.tarfile.getmembers())
2555 def printdir(self):
2556 self.tarfile.list()
2557 def testzip(self):
2558 return
2559 def getinfo(self, name):
2560 return self.tarfile.getmember(name)
2561 def read(self, name):
2562 return self.tarfile.extractfile(self.tarfile.getmember(name)).read()
2563 def write(self, filename, arcname=None, compress_type=None):
2564 self.tarfile.add(filename, arcname)
2565 def writestr(self, zinfo, bytes):
2566 try:
2567 from cStringIO import StringIO
2568 except ImportError:
2569 from StringIO import StringIO
2570 import calendar
2571 tinfo = TarInfo(zinfo.filename)
2572 tinfo.size = len(bytes)
2573 tinfo.mtime = calendar.timegm(zinfo.date_time)
2574 self.tarfile.addfile(tinfo, StringIO(bytes))
2575 def close(self):
2576 self.tarfile.close()
2577#class TarFileCompat
2578
2579#--------------------
2580# exported functions
2581#--------------------
2582def is_tarfile(name):
2583 """Return True if name points to a tar archive that we
2584 are able to handle, else return False.
2585 """
2586 try:
2587 t = open(name)
2588 t.close()
2589 return True
2590 except TarError:
2591 return False
2592
2593bltn_open = open
2594open = TarFile.open
259536
=== removed file 'duplicity/urlparse_2_5.py'
--- duplicity/urlparse_2_5.py 2011-10-08 16:22:30 +0000
+++ duplicity/urlparse_2_5.py 1970-01-01 00:00:00 +0000
@@ -1,385 +0,0 @@
1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2
3"""Parse (absolute and relative) URLs.
4
5See RFC 1808: "Relative Uniform Resource Locators", by R. Fielding,
6UC Irvine, June 1995.
7"""
8
9__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
10 "urlsplit", "urlunsplit"]
11
12# A classification of schemes ('' means apply by default)
13uses_relative = ['ftp', 'ftps', 'http', 'gopher', 'nntp',
14 'wais', 'file', 'https', 'shttp', 'mms',
15 'prospero', 'rtsp', 'rtspu', '', 'sftp', 'imap', 'imaps']
16uses_netloc = ['ftp', 'ftps', 'http', 'gopher', 'nntp', 'telnet',
17 'wais', 'file', 'mms', 'https', 'shttp',
18 'snews', 'prospero', 'rtsp', 'rtspu', 'rsync', '',
19 'svn', 'svn+ssh', 'sftp', 'imap', 'imaps']
20non_hierarchical = ['gopher', 'hdl', 'mailto', 'news',
21 'telnet', 'wais', 'snews', 'sip', 'sips', 'imap', 'imaps']
22uses_params = ['ftp', 'ftps', 'hdl', 'prospero', 'http',
23 'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips',
24 'mms', '', 'sftp', 'imap', 'imaps']
25uses_query = ['http', 'wais', 'https', 'shttp', 'mms',
26 'gopher', 'rtsp', 'rtspu', 'sip', 'sips', 'imap', 'imaps', '']
27uses_fragment = ['ftp', 'ftps', 'hdl', 'http', 'gopher', 'news',
28 'nntp', 'wais', 'https', 'shttp', 'snews',
29 'file', 'prospero', '']
30
31# Characters valid in scheme names
32scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
33 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
34 '0123456789'
35 '+-.')
36
37MAX_CACHE_SIZE = 20
38_parse_cache = {}
39
40def clear_cache():
41 """Clear the parse cache."""
42 global _parse_cache
43 _parse_cache = {}
44
45import string
46def _rsplit(str, delim, numsplit):
47 parts = string.split(str, delim)
48 if len(parts) <= numsplit + 1:
49 return parts
50 else:
51 left = string.join(parts[0:-numsplit], delim)
52 right = string.join(parts[len(parts)-numsplit:], delim)
53 return [left, right]
54
55class BaseResult(tuple):
56 """Base class for the parsed result objects.
57
58 This provides the attributes shared by the two derived result
59 objects as read-only properties. The derived classes are
60 responsible for checking the right number of arguments were
61 supplied to the constructor.
62
63 """
64
65 __slots__ = ()
66
67 # Attributes that access the basic components of the URL:
68
69 def get_scheme(self):
70 return self[0]
71 scheme = property(get_scheme)
72
73 def get_netloc(self):
74 return self[1]
75 netloc = property(get_netloc)
76
77 def get_path(self):
78 return self[2]
79 path = property(get_path)
80
81 def get_query(self):
82 return self[-2]
83 query = property(get_query)
84
85 def get_fragment(self):
86 return self[-1]
87 fragment = property(get_fragment)
88
89 # Additional attributes that provide access to parsed-out portions
90 # of the netloc:
91
92 def get_username(self):
93 netloc = self.netloc
94 if "@" in netloc:
95 userinfo = _rsplit(netloc, "@", 1)[0]
96 if ":" in userinfo:
97 userinfo = userinfo.split(":", 1)[0]
98 return userinfo
99 return None
100 username = property(get_username)
101
102 def get_password(self):
103 netloc = self.netloc
104 if "@" in netloc:
105 userinfo = _rsplit(netloc, "@", 1)[0]
106 if ":" in userinfo:
107 return userinfo.split(":", 1)[1]
108 return None
109 password = property(get_password)
110
111 def get_hostname(self):
112 netloc = self.netloc.split('@')[-1]
113 if '[' in netloc and ']' in netloc:
114 return netloc.split(']')[0][1:].lower()
115 elif ':' in netloc:
116 return netloc.split(':')[0].lower()
117 elif netloc == '':
118 return None
119 else:
120 return netloc.lower()
121 hostname = property(get_hostname)
122
123 def get_port(self):
124 netloc = self.netloc.split('@')[-1].split(']')[-1]
125 if ":" in netloc:
126 port = netloc.split(":", 1)[1]
127 return int(port, 10)
128 return None
129 port = property(get_port)
130
131
132class SplitResult(BaseResult):
133
134 __slots__ = ()
135
136 def __new__(cls, scheme, netloc, path, query, fragment):
137 return BaseResult.__new__(
138 cls, (scheme, netloc, path, query, fragment))
139
140 def geturl(self):
141 return urlunsplit(self)
142
143
144class ParseResult(BaseResult):
145
146 __slots__ = ()
147
148 def __new__(cls, scheme, netloc, path, params, query, fragment):
149 return BaseResult.__new__(
150 cls, (scheme, netloc, path, params, query, fragment))
151
152 def get_params(self):
153 return self[3]
154 params = property(get_params)
155
156 def geturl(self):
157 return urlunparse(self)
158
159
160def urlparse(url, scheme='', allow_fragments=True):
161 """Parse a URL into 6 components:
162 <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
163 Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
164 Note that we don't break the components up in smaller bits
165 (e.g. netloc is a single string) and we don't expand % escapes."""
166 tuple = urlsplit(url, scheme, allow_fragments)
167 scheme, netloc, url, query, fragment = tuple
168 if scheme in uses_params and ';' in url:
169 url, params = _splitparams(url)
170 else:
171 params = ''
172 return ParseResult(scheme, netloc, url, params, query, fragment)
173
174def _splitparams(url):
175 if '/' in url:
176 i = url.find(';', url.rfind('/'))
177 if i < 0:
178 return url, ''
179 else:
180 i = url.find(';')
181 return url[:i], url[i+1:]
182
183def _splitnetloc(url, start=0):
184 for c in '/?#': # the order is important!
185 delim = url.find(c, start)
186 if delim >= 0:
187 break
188 else:
189 delim = len(url)
190 return url[start:delim], url[delim:]
191
192def urlsplit(url, scheme='', allow_fragments=True):
193 """Parse a URL into 5 components:
194 <scheme>://<netloc>/<path>?<query>#<fragment>
195 Return a 5-tuple: (scheme, netloc, path, query, fragment).
196 Note that we don't break the components up in smaller bits
197 (e.g. netloc is a single string) and we don't expand % escapes."""
198 allow_fragments = bool(allow_fragments)
199 key = url, scheme, allow_fragments
200 cached = _parse_cache.get(key, None)
201 if cached:
202 return cached
203 if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
204 clear_cache()
205 netloc = query = fragment = ''
206 i = url.find(':')
207 if i > 0:
208 if url[:i] == 'http': # optimize the common case
209 scheme = url[:i].lower()
210 url = url[i+1:]
211 if url[:2] == '//':
212 netloc, url = _splitnetloc(url, 2)
213 if allow_fragments and '#' in url:
214 url, fragment = url.split('#', 1)
215 if '?' in url:
216 url, query = url.split('?', 1)
217 v = SplitResult(scheme, netloc, url, query, fragment)
218 _parse_cache[key] = v
219 return v
220 for c in url[:i]:
221 if c not in scheme_chars:
222 break
223 else:
224 scheme, url = url[:i].lower(), url[i+1:]
225 if scheme in uses_netloc and url[:2] == '//':
226 netloc, url = _splitnetloc(url, 2)
227 if allow_fragments and scheme in uses_fragment and '#' in url:
228 url, fragment = url.split('#', 1)
229 if scheme in uses_query and '?' in url:
230 url, query = url.split('?', 1)
231 v = SplitResult(scheme, netloc, url, query, fragment)
232 _parse_cache[key] = v
233 return v
234
235def urlunparse((scheme, netloc, url, params, query, fragment)):
236 """Put a parsed URL back together again. This may result in a
237 slightly different, but equivalent URL, if the URL that was parsed
238 originally had redundant delimiters, e.g. a ? with an empty query
239 (the draft states that these are equivalent)."""
240 if params:
241 url = "%s;%s" % (url, params)
242 return urlunsplit((scheme, netloc, url, query, fragment))
243
244def urlunsplit((scheme, netloc, url, query, fragment)):
245 if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
246 if url and url[:1] != '/': url = '/' + url
247 url = '//' + (netloc or '') + url
248 if scheme:
249 url = scheme + ':' + url
250 if query:
251 url = url + '?' + query
252 if fragment:
253 url = url + '#' + fragment
254 return url
255
256def urljoin(base, url, allow_fragments=True):
257 """Join a base URL and a possibly relative URL to form an absolute
258 interpretation of the latter."""
259 if not base:
260 return url
261 if not url:
262 return base
263 bscheme, bnetloc, bpath, bparams, bquery, bfragment = urlparse(base, '', allow_fragments) #@UnusedVariable
264 scheme, netloc, path, params, query, fragment = urlparse(url, bscheme, allow_fragments)
265 if scheme != bscheme or scheme not in uses_relative:
266 return url
267 if scheme in uses_netloc:
268 if netloc:
269 return urlunparse((scheme, netloc, path,
270 params, query, fragment))
271 netloc = bnetloc
272 if path[:1] == '/':
273 return urlunparse((scheme, netloc, path,
274 params, query, fragment))
275 if not (path or params or query):
276 return urlunparse((scheme, netloc, bpath,
277 bparams, bquery, fragment))
278 segments = bpath.split('/')[:-1] + path.split('/')
279 # XXX The stuff below is bogus in various ways...
280 if segments[-1] == '.':
281 segments[-1] = ''
282 while '.' in segments:
283 segments.remove('.')
284 while 1:
285 i = 1
286 n = len(segments) - 1
287 while i < n:
288 if (segments[i] == '..'
289 and segments[i-1] not in ('', '..')):
290 del segments[i-1:i+1]
291 break
292 i = i+1
293 else:
294 break
295 if segments == ['', '..']:
296 segments[-1] = ''
297 elif len(segments) >= 2 and segments[-1] == '..':
298 segments[-2:] = ['']
299 return urlunparse((scheme, netloc, '/'.join(segments),
300 params, query, fragment))
301
302def urldefrag(url):
303 """Removes any existing fragment from URL.
304
305 Returns a tuple of the defragmented URL and the fragment. If
306 the URL contained no fragments, the second element is the
307 empty string.
308 """
309 if '#' in url:
310 s, n, p, a, q, frag = urlparse(url)
311 defrag = urlunparse((s, n, p, a, q, ''))
312 return defrag, frag
313 else:
314 return url, ''
315
316
317test_input = """
318 http://a/b/c/d
319
320 g:h = <URL:g:h>
321 http:g = <URL:http://a/b/c/g>
322 http: = <URL:http://a/b/c/d>
323 g = <URL:http://a/b/c/g>
324 ./g = <URL:http://a/b/c/g>
325 g/ = <URL:http://a/b/c/g/>
326 /g = <URL:http://a/g>
327 //g = <URL:http://g>
328 ?y = <URL:http://a/b/c/d?y>
329 g?y = <URL:http://a/b/c/g?y>
330 g?y/./x = <URL:http://a/b/c/g?y/./x>
331 . = <URL:http://a/b/c/>
332 ./ = <URL:http://a/b/c/>
333 .. = <URL:http://a/b/>
334 ../ = <URL:http://a/b/>
335 ../g = <URL:http://a/b/g>
336 ../.. = <URL:http://a/>
337 ../../g = <URL:http://a/g>
338 ../../../g = <URL:http://a/../g>
339 ./../g = <URL:http://a/b/g>
340 ./g/. = <URL:http://a/b/c/g/>
341 /./g = <URL:http://a/./g>
342 g/./h = <URL:http://a/b/c/g/h>
343 g/../h = <URL:http://a/b/c/h>
344 http:g = <URL:http://a/b/c/g>
345 http: = <URL:http://a/b/c/d>
346 http:?y = <URL:http://a/b/c/d?y>
347 http:g?y = <URL:http://a/b/c/g?y>
348 http:g?y/./x = <URL:http://a/b/c/g?y/./x>
349"""
350
351def test():
352 import sys
353 base = ''
354 if sys.argv[1:]:
355 fn = sys.argv[1]
356 if fn == '-':
357 fp = sys.stdin
358 else:
359 fp = open(fn)
360 else:
361 try:
362 from cStringIO import StringIO
363 except ImportError:
364 from StringIO import StringIO
365 fp = StringIO(test_input)
366 while 1:
367 line = fp.readline()
368 if not line: break
369 words = line.split()
370 if not words:
371 continue
372 url = words[0]
373 parts = urlparse(url)
374 print '%-10s : %s' % (url, parts)
375 abs = urljoin(base, url)
376 if not base:
377 base = abs
378 wrapped = '<URL:%s>' % abs
379 print '%-10s = %s' % (url, wrapped)
380 if len(words) == 3 and words[1] == '=':
381 if wrapped != words[2]:
382 print 'EXPECTED', words[2], '!!!!!!!!!!'
383
384if __name__ == '__main__':
385 test()
3860
=== modified file 'po/POTFILES.in'
--- po/POTFILES.in 2014-01-24 14:44:45 +0000
+++ po/POTFILES.in 2014-04-16 20:51:42 +0000
@@ -7,7 +7,6 @@
7duplicity/selection.py7duplicity/selection.py
8duplicity/globals.py8duplicity/globals.py
9duplicity/commandline.py9duplicity/commandline.py
10duplicity/urlparse_2_5.py
11duplicity/dup_temp.py10duplicity/dup_temp.py
12duplicity/backend.py11duplicity/backend.py
13duplicity/asyncscheduler.py12duplicity/asyncscheduler.py
1413
=== modified file 'po/duplicity.pot'
--- po/duplicity.pot 2014-01-24 14:44:45 +0000
+++ po/duplicity.pot 2014-04-16 20:51:42 +0000
@@ -8,7 +8,7 @@
8msgstr ""8msgstr ""
9"Project-Id-Version: PACKAGE VERSION\n"9"Project-Id-Version: PACKAGE VERSION\n"
10"Report-Msgid-Bugs-To: Kenneth Loafman <kenneth@loafman.com>\n"10"Report-Msgid-Bugs-To: Kenneth Loafman <kenneth@loafman.com>\n"
11"POT-Creation-Date: 2014-01-24 06:47-0600\n"11"POT-Creation-Date: 2014-04-16 16:34-0400\n"
12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14"Language-Team: LANGUAGE <LL@li.org>\n"14"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -118,194 +118,194 @@
118msgid "Processed volume %d of %d"118msgid "Processed volume %d of %d"
119msgstr ""119msgstr ""
120120
121#: ../bin/duplicity:756121#: ../bin/duplicity:765
122#, python-format122#, python-format
123msgid "Invalid data - %s hash mismatch for file:"123msgid "Invalid data - %s hash mismatch for file:"
124msgstr ""124msgstr ""
125125
126#: ../bin/duplicity:758126#: ../bin/duplicity:767
127#, python-format127#, python-format
128msgid "Calculated hash: %s"128msgid "Calculated hash: %s"
129msgstr ""129msgstr ""
130130
131#: ../bin/duplicity:759131#: ../bin/duplicity:768
132#, python-format132#, python-format
133msgid "Manifest hash: %s"133msgid "Manifest hash: %s"
134msgstr ""134msgstr ""
135135
136#: ../bin/duplicity:797136#: ../bin/duplicity:806
137#, python-format137#, python-format
138msgid "Volume was signed by key %s, not %s"138msgid "Volume was signed by key %s, not %s"
139msgstr ""139msgstr ""
140140
141#: ../bin/duplicity:827141#: ../bin/duplicity:836
142#, python-format142#, python-format
143msgid "Verify complete: %s, %s."143msgid "Verify complete: %s, %s."
144msgstr ""144msgstr ""
145145
146#: ../bin/duplicity:828146#: ../bin/duplicity:837
147#, python-format147#, python-format
148msgid "%d file compared"148msgid "%d file compared"
149msgid_plural "%d files compared"149msgid_plural "%d files compared"
150msgstr[0] ""150msgstr[0] ""
151msgstr[1] ""151msgstr[1] ""
152152
153#: ../bin/duplicity:830153#: ../bin/duplicity:839
154#, python-format154#, python-format
155msgid "%d difference found"155msgid "%d difference found"
156msgid_plural "%d differences found"156msgid_plural "%d differences found"
157msgstr[0] ""157msgstr[0] ""
158msgstr[1] ""158msgstr[1] ""
159159
160#: ../bin/duplicity:849160#: ../bin/duplicity:858
161msgid "No extraneous files found, nothing deleted in cleanup."161msgid "No extraneous files found, nothing deleted in cleanup."
162msgstr ""162msgstr ""
163163
164#: ../bin/duplicity:854164#: ../bin/duplicity:863
165msgid "Deleting this file from backend:"165msgid "Deleting this file from backend:"
166msgid_plural "Deleting these files from backend:"166msgid_plural "Deleting these files from backend:"
167msgstr[0] ""167msgstr[0] ""
168msgstr[1] ""168msgstr[1] ""
169169
170#: ../bin/duplicity:866170#: ../bin/duplicity:875
171msgid "Found the following file to delete:"171msgid "Found the following file to delete:"
172msgid_plural "Found the following files to delete:"172msgid_plural "Found the following files to delete:"
173msgstr[0] ""173msgstr[0] ""
174msgstr[1] ""174msgstr[1] ""
175175
176#: ../bin/duplicity:870176#: ../bin/duplicity:879
177msgid "Run duplicity again with the --force option to actually delete."177msgid "Run duplicity again with the --force option to actually delete."
178msgstr ""178msgstr ""
179179
180#: ../bin/duplicity:913180#: ../bin/duplicity:922
181msgid "There are backup set(s) at time(s):"181msgid "There are backup set(s) at time(s):"
182msgstr ""182msgstr ""
183183
184#: ../bin/duplicity:915184#: ../bin/duplicity:924
185msgid "Which can't be deleted because newer sets depend on them."185msgid "Which can't be deleted because newer sets depend on them."
186msgstr ""186msgstr ""
187187
188#: ../bin/duplicity:919188#: ../bin/duplicity:928
189msgid ""189msgid ""
190"Current active backup chain is older than specified time. However, it will "190"Current active backup chain is older than specified time. However, it will "
191"not be deleted. To remove all your backups, manually purge the repository."191"not be deleted. To remove all your backups, manually purge the repository."
192msgstr ""192msgstr ""
193193
194#: ../bin/duplicity:925194#: ../bin/duplicity:934
195msgid "No old backup sets found, nothing deleted."195msgid "No old backup sets found, nothing deleted."
196msgstr ""196msgstr ""
197197
198#: ../bin/duplicity:928198#: ../bin/duplicity:937
199msgid "Deleting backup chain at time:"199msgid "Deleting backup chain at time:"
200msgid_plural "Deleting backup chains at times:"200msgid_plural "Deleting backup chains at times:"
201msgstr[0] ""201msgstr[0] ""
202msgstr[1] ""202msgstr[1] ""
203203
204#: ../bin/duplicity:939204#: ../bin/duplicity:948
205#, python-format205#, python-format
206msgid "Deleting incremental signature chain %s"206msgid "Deleting incremental signature chain %s"
207msgstr ""207msgstr ""
208208
209#: ../bin/duplicity:941209#: ../bin/duplicity:950
210#, python-format210#, python-format
211msgid "Deleting incremental backup chain %s"211msgid "Deleting incremental backup chain %s"
212msgstr ""212msgstr ""
213213
214#: ../bin/duplicity:944214#: ../bin/duplicity:953
215#, python-format215#, python-format
216msgid "Deleting complete signature chain %s"216msgid "Deleting complete signature chain %s"
217msgstr ""217msgstr ""
218218
219#: ../bin/duplicity:946219#: ../bin/duplicity:955
220#, python-format220#, python-format
221msgid "Deleting complete backup chain %s"221msgid "Deleting complete backup chain %s"
222msgstr ""222msgstr ""
223223
224#: ../bin/duplicity:952224#: ../bin/duplicity:961
225msgid "Found old backup chain at the following time:"225msgid "Found old backup chain at the following time:"
226msgid_plural "Found old backup chains at the following times:"226msgid_plural "Found old backup chains at the following times:"
227msgstr[0] ""227msgstr[0] ""
228msgstr[1] ""228msgstr[1] ""
229229
230#: ../bin/duplicity:956230#: ../bin/duplicity:965
231msgid "Rerun command with --force option to actually delete."231msgid "Rerun command with --force option to actually delete."
232msgstr ""232msgstr ""
233233
234#: ../bin/duplicity:1033234#: ../bin/duplicity:1042
235#, python-format235#, python-format
236msgid "Deleting local %s (not authoritative at backend)."236msgid "Deleting local %s (not authoritative at backend)."
237msgstr ""237msgstr ""
238238
239#: ../bin/duplicity:1037239#: ../bin/duplicity:1046
240#, python-format240#, python-format
241msgid "Unable to delete %s: %s"241msgid "Unable to delete %s: %s"
242msgstr ""242msgstr ""
243243
244#: ../bin/duplicity:1065 ../duplicity/dup_temp.py:263244#: ../bin/duplicity:1074 ../duplicity/dup_temp.py:263
245#, python-format245#, python-format
246msgid "Failed to read %s: %s"246msgid "Failed to read %s: %s"
247msgstr ""247msgstr ""
248248
249#: ../bin/duplicity:1079249#: ../bin/duplicity:1088
250#, python-format250#, python-format
251msgid "Copying %s to local cache."251msgid "Copying %s to local cache."
252msgstr ""252msgstr ""
253253
254#: ../bin/duplicity:1127254#: ../bin/duplicity:1136
255msgid "Local and Remote metadata are synchronized, no sync needed."255msgid "Local and Remote metadata are synchronized, no sync needed."
256msgstr ""256msgstr ""
257257
258#: ../bin/duplicity:1132258#: ../bin/duplicity:1141
259msgid "Synchronizing remote metadata to local cache..."259msgid "Synchronizing remote metadata to local cache..."
260msgstr ""260msgstr ""
261261
262#: ../bin/duplicity:1145262#: ../bin/duplicity:1156
263msgid "Sync would copy the following from remote to local:"263msgid "Sync would copy the following from remote to local:"
264msgstr ""264msgstr ""
265265
266#: ../bin/duplicity:1148266#: ../bin/duplicity:1159
267msgid "Sync would remove the following spurious local files:"267msgid "Sync would remove the following spurious local files:"
268msgstr ""268msgstr ""
269269
270#: ../bin/duplicity:1191270#: ../bin/duplicity:1202
271msgid "Unable to get free space on temp."271msgid "Unable to get free space on temp."
272msgstr ""272msgstr ""
273273
274#: ../bin/duplicity:1199274#: ../bin/duplicity:1210
275#, python-format275#, python-format
276msgid "Temp space has %d available, backup needs approx %d."276msgid "Temp space has %d available, backup needs approx %d."
277msgstr ""277msgstr ""
278278
279#: ../bin/duplicity:1202279#: ../bin/duplicity:1213
280#, python-format280#, python-format
281msgid "Temp has %d available, backup will use approx %d."281msgid "Temp has %d available, backup will use approx %d."
282msgstr ""282msgstr ""
283283
284#: ../bin/duplicity:1210284#: ../bin/duplicity:1221
285msgid "Unable to get max open files."285msgid "Unable to get max open files."
286msgstr ""286msgstr ""
287287
288#: ../bin/duplicity:1214288#: ../bin/duplicity:1225
289#, python-format289#, python-format
290msgid ""290msgid ""
291"Max open files of %s is too low, should be >= 1024.\n"291"Max open files of %s is too low, should be >= 1024.\n"
292"Use 'ulimit -n 1024' or higher to correct.\n"292"Use 'ulimit -n 1024' or higher to correct.\n"
293msgstr ""293msgstr ""
294294
295#: ../bin/duplicity:1263295#: ../bin/duplicity:1274
296msgid ""296msgid ""
297"RESTART: The first volume failed to upload before termination.\n"297"RESTART: The first volume failed to upload before termination.\n"
298" Restart is impossible...starting backup from beginning."298" Restart is impossible...starting backup from beginning."
299msgstr ""299msgstr ""
300300
301#: ../bin/duplicity:1269301#: ../bin/duplicity:1280
302#, python-format302#, python-format
303msgid ""303msgid ""
304"RESTART: Volumes %d to %d failed to upload before termination.\n"304"RESTART: Volumes %d to %d failed to upload before termination.\n"
305" Restarting backup at volume %d."305" Restarting backup at volume %d."
306msgstr ""306msgstr ""
307307
308#: ../bin/duplicity:1276308#: ../bin/duplicity:1287
309#, python-format309#, python-format
310msgid ""310msgid ""
311"RESTART: Impossible backup state: manifest has %d vols, remote has %d vols.\n"311"RESTART: Impossible backup state: manifest has %d vols, remote has %d vols.\n"
@@ -314,7 +314,7 @@
314" backup then restart the backup from the beginning."314" backup then restart the backup from the beginning."
315msgstr ""315msgstr ""
316316
317#: ../bin/duplicity:1298317#: ../bin/duplicity:1309
318msgid ""318msgid ""
319"\n"319"\n"
320"PYTHONOPTIMIZE in the environment causes duplicity to fail to\n"320"PYTHONOPTIMIZE in the environment causes duplicity to fail to\n"
@@ -324,59 +324,59 @@
324"See https://bugs.launchpad.net/duplicity/+bug/931175\n"324"See https://bugs.launchpad.net/duplicity/+bug/931175\n"
325msgstr ""325msgstr ""
326326
327#: ../bin/duplicity:1388327#: ../bin/duplicity:1400
328#, python-format328#, python-format
329msgid "Last %s backup left a partial set, restarting."329msgid "Last %s backup left a partial set, restarting."
330msgstr ""330msgstr ""
331331
332#: ../bin/duplicity:1392332#: ../bin/duplicity:1404
333#, python-format333#, python-format
334msgid "Cleaning up previous partial %s backup set, restarting."334msgid "Cleaning up previous partial %s backup set, restarting."
335msgstr ""335msgstr ""
336336
337#: ../bin/duplicity:1403337#: ../bin/duplicity:1415
338msgid "Last full backup date:"338msgid "Last full backup date:"
339msgstr ""339msgstr ""
340340
341#: ../bin/duplicity:1405341#: ../bin/duplicity:1417
342msgid "Last full backup date: none"342msgid "Last full backup date: none"
343msgstr ""343msgstr ""
344344
345#: ../bin/duplicity:1407345#: ../bin/duplicity:1419
346msgid "Last full backup is too old, forcing full backup"346msgid "Last full backup is too old, forcing full backup"
347msgstr ""347msgstr ""
348348
349#: ../bin/duplicity:1450349#: ../bin/duplicity:1462
350msgid ""350msgid ""
351"When using symmetric encryption, the signing passphrase must equal the "351"When using symmetric encryption, the signing passphrase must equal the "
352"encryption passphrase."352"encryption passphrase."
353msgstr ""353msgstr ""
354354
355#: ../bin/duplicity:1503355#: ../bin/duplicity:1515
356msgid "INT intercepted...exiting."356msgid "INT intercepted...exiting."
357msgstr ""357msgstr ""
358358
359#: ../bin/duplicity:1511359#: ../bin/duplicity:1523
360#, python-format360#, python-format
361msgid "GPG error detail: %s"361msgid "GPG error detail: %s"
362msgstr ""362msgstr ""
363363
364#: ../bin/duplicity:1521364#: ../bin/duplicity:1533
365#, python-format365#, python-format
366msgid "User error detail: %s"366msgid "User error detail: %s"
367msgstr ""367msgstr ""
368368
369#: ../bin/duplicity:1531369#: ../bin/duplicity:1543
370#, python-format370#, python-format
371msgid "Backend error detail: %s"371msgid "Backend error detail: %s"
372msgstr ""372msgstr ""
373373
374#: ../bin/rdiffdir:59 ../duplicity/commandline.py:237374#: ../bin/rdiffdir:56 ../duplicity/commandline.py:237
375#, python-format375#, python-format
376msgid "Error opening file %s"376msgid "Error opening file %s"
377msgstr ""377msgstr ""
378378
379#: ../bin/rdiffdir:122379#: ../bin/rdiffdir:119
380#, python-format380#, python-format
381msgid "File %s already exists, will not overwrite."381msgid "File %s already exists, will not overwrite."
382msgstr ""382msgstr ""
@@ -493,8 +493,8 @@
493#. Used in usage help to represent a Unix-style path name. Example:493#. Used in usage help to represent a Unix-style path name. Example:
494#. --archive-dir <path>494#. --archive-dir <path>
495#: ../duplicity/commandline.py:258 ../duplicity/commandline.py:268495#: ../duplicity/commandline.py:258 ../duplicity/commandline.py:268
496#: ../duplicity/commandline.py:285 ../duplicity/commandline.py:342496#: ../duplicity/commandline.py:285 ../duplicity/commandline.py:351
497#: ../duplicity/commandline.py:530 ../duplicity/commandline.py:746497#: ../duplicity/commandline.py:548 ../duplicity/commandline.py:764
498msgid "path"498msgid "path"
499msgstr ""499msgstr ""
500500
@@ -505,8 +505,8 @@
505#. Used in usage help to represent an ID for a GnuPG key. Example:505#. Used in usage help to represent an ID for a GnuPG key. Example:
506#. --encrypt-key <gpg_key_id>506#. --encrypt-key <gpg_key_id>
507#: ../duplicity/commandline.py:280 ../duplicity/commandline.py:287507#: ../duplicity/commandline.py:280 ../duplicity/commandline.py:287
508#: ../duplicity/commandline.py:362 ../duplicity/commandline.py:511508#: ../duplicity/commandline.py:371 ../duplicity/commandline.py:529
509#: ../duplicity/commandline.py:719509#: ../duplicity/commandline.py:737
510msgid "gpg-key-id"510msgid "gpg-key-id"
511msgstr ""511msgstr ""
512512
@@ -514,42 +514,42 @@
514#. matching one or more files, as described in the documentation.514#. matching one or more files, as described in the documentation.
515#. Example:515#. Example:
516#. --exclude <shell_pattern>516#. --exclude <shell_pattern>
517#: ../duplicity/commandline.py:295 ../duplicity/commandline.py:388517#: ../duplicity/commandline.py:295 ../duplicity/commandline.py:397
518#: ../duplicity/commandline.py:769518#: ../duplicity/commandline.py:787
519msgid "shell_pattern"519msgid "shell_pattern"
520msgstr ""520msgstr ""
521521
522#. Used in usage help to represent the name of a file. Example:522#. Used in usage help to represent the name of a file. Example:
523#. --log-file <filename>523#. --log-file <filename>
524#: ../duplicity/commandline.py:301 ../duplicity/commandline.py:308524#: ../duplicity/commandline.py:301 ../duplicity/commandline.py:308
525#: ../duplicity/commandline.py:313 ../duplicity/commandline.py:390525#: ../duplicity/commandline.py:313 ../duplicity/commandline.py:399
526#: ../duplicity/commandline.py:395 ../duplicity/commandline.py:406526#: ../duplicity/commandline.py:404 ../duplicity/commandline.py:415
527#: ../duplicity/commandline.py:715527#: ../duplicity/commandline.py:733
528msgid "filename"528msgid "filename"
529msgstr ""529msgstr ""
530530
531#. Used in usage help to represent a regular expression (regexp).531#. Used in usage help to represent a regular expression (regexp).
532#: ../duplicity/commandline.py:320 ../duplicity/commandline.py:397532#: ../duplicity/commandline.py:320 ../duplicity/commandline.py:406
533msgid "regular_expression"533msgid "regular_expression"
534msgstr ""534msgstr ""
535535
536#. Used in usage help to represent a time spec for a previous536#. Used in usage help to represent a time spec for a previous
537#. point in time, as described in the documentation. Example:537#. point in time, as described in the documentation. Example:
538#. duplicity remove-older-than time [options] target_url538#. duplicity remove-older-than time [options] target_url
539#: ../duplicity/commandline.py:354 ../duplicity/commandline.py:462539#: ../duplicity/commandline.py:363 ../duplicity/commandline.py:474
540#: ../duplicity/commandline.py:801540#: ../duplicity/commandline.py:819
541msgid "time"541msgid "time"
542msgstr ""542msgstr ""
543543
544#. Used in usage help. (Should be consistent with the "Options:"544#. Used in usage help. (Should be consistent with the "Options:"
545#. header.) Example:545#. header.) Example:
546#. duplicity [full|incremental] [options] source_dir target_url546#. duplicity [full|incremental] [options] source_dir target_url
547#: ../duplicity/commandline.py:358 ../duplicity/commandline.py:465547#: ../duplicity/commandline.py:367 ../duplicity/commandline.py:477
548#: ../duplicity/commandline.py:522 ../duplicity/commandline.py:734548#: ../duplicity/commandline.py:540 ../duplicity/commandline.py:752
549msgid "options"549msgid "options"
550msgstr ""550msgstr ""
551551
552#: ../duplicity/commandline.py:373552#: ../duplicity/commandline.py:382
553#, python-format553#, python-format
554msgid ""554msgid ""
555"Running in 'ignore errors' mode due to %s; please re-consider if this was "555"Running in 'ignore errors' mode due to %s; please re-consider if this was "
@@ -557,150 +557,152 @@
557msgstr ""557msgstr ""
558558
559#. Used in usage help to represent an imap mailbox559#. Used in usage help to represent an imap mailbox
560#: ../duplicity/commandline.py:386560#: ../duplicity/commandline.py:395
561msgid "imap_mailbox"561msgid "imap_mailbox"
562msgstr ""562msgstr ""
563563
564#: ../duplicity/commandline.py:400564#: ../duplicity/commandline.py:409
565msgid "file_descriptor"565msgid "file_descriptor"
566msgstr ""566msgstr ""
567567
568#. Used in usage help to represent a desired number of568#. Used in usage help to represent a desired number of
569#. something. Example:569#. something. Example:
570#. --num-retries <number>570#. --num-retries <number>
571#: ../duplicity/commandline.py:411 ../duplicity/commandline.py:433571#: ../duplicity/commandline.py:420 ../duplicity/commandline.py:442
572#: ../duplicity/commandline.py:448 ../duplicity/commandline.py:486572#: ../duplicity/commandline.py:454 ../duplicity/commandline.py:460
573#: ../duplicity/commandline.py:560 ../duplicity/commandline.py:729573#: ../duplicity/commandline.py:498 ../duplicity/commandline.py:503
574#: ../duplicity/commandline.py:507 ../duplicity/commandline.py:578
575#: ../duplicity/commandline.py:747
574msgid "number"576msgid "number"
575msgstr ""577msgstr ""
576578
577#. Used in usage help (noun)579#. Used in usage help (noun)
578#: ../duplicity/commandline.py:414580#: ../duplicity/commandline.py:423
579msgid "backup name"581msgid "backup name"
580msgstr ""582msgstr ""
581583
582#. noun584#. noun
583#: ../duplicity/commandline.py:495 ../duplicity/commandline.py:498585#: ../duplicity/commandline.py:513 ../duplicity/commandline.py:516
584#: ../duplicity/commandline.py:501 ../duplicity/commandline.py:700586#: ../duplicity/commandline.py:519 ../duplicity/commandline.py:718
585msgid "command"587msgid "command"
586msgstr ""588msgstr ""
587589
588#: ../duplicity/commandline.py:519590#: ../duplicity/commandline.py:537
589msgid "paramiko|pexpect"591msgid "paramiko|pexpect"
590msgstr ""592msgstr ""
591593
592#: ../duplicity/commandline.py:525594#: ../duplicity/commandline.py:543
593msgid "pem formatted bundle of certificate authorities"595msgid "pem formatted bundle of certificate authorities"
594msgstr ""596msgstr ""
595597
596#. Used in usage help. Example:598#. Used in usage help. Example:
597#. --timeout <seconds>599#. --timeout <seconds>
598#: ../duplicity/commandline.py:535 ../duplicity/commandline.py:763600#: ../duplicity/commandline.py:553 ../duplicity/commandline.py:781
599msgid "seconds"601msgid "seconds"
600msgstr ""602msgstr ""
601603
602#. abbreviation for "character" (noun)604#. abbreviation for "character" (noun)
603#: ../duplicity/commandline.py:541 ../duplicity/commandline.py:697605#: ../duplicity/commandline.py:559 ../duplicity/commandline.py:715
604msgid "char"606msgid "char"
605msgstr ""607msgstr ""
606608
607#: ../duplicity/commandline.py:663609#: ../duplicity/commandline.py:681
608#, python-format610#, python-format
609msgid "Using archive dir: %s"611msgid "Using archive dir: %s"
610msgstr ""612msgstr ""
611613
612#: ../duplicity/commandline.py:664614#: ../duplicity/commandline.py:682
613#, python-format615#, python-format
614msgid "Using backup name: %s"616msgid "Using backup name: %s"
615msgstr ""617msgstr ""
616618
617#: ../duplicity/commandline.py:671619#: ../duplicity/commandline.py:689
618#, python-format620#, python-format
619msgid "Command line error: %s"621msgid "Command line error: %s"
620msgstr ""622msgstr ""
621623
622#: ../duplicity/commandline.py:672624#: ../duplicity/commandline.py:690
623msgid "Enter 'duplicity --help' for help screen."625msgid "Enter 'duplicity --help' for help screen."
624msgstr ""626msgstr ""
625627
626#. Used in usage help to represent a Unix-style path name. Example:628#. Used in usage help to represent a Unix-style path name. Example:
627#. rsync://user[:password]@other_host[:port]//absolute_path629#. rsync://user[:password]@other_host[:port]//absolute_path
628#: ../duplicity/commandline.py:685630#: ../duplicity/commandline.py:703
629msgid "absolute_path"631msgid "absolute_path"
630msgstr ""632msgstr ""
631633
632#. Used in usage help. Example:634#. Used in usage help. Example:
633#. tahoe://alias/some_dir635#. tahoe://alias/some_dir
634#: ../duplicity/commandline.py:689636#: ../duplicity/commandline.py:707
635msgid "alias"637msgid "alias"
636msgstr ""638msgstr ""
637639
638#. Used in help to represent a "bucket name" for Amazon Web640#. Used in help to represent a "bucket name" for Amazon Web
639#. Services' Simple Storage Service (S3). Example:641#. Services' Simple Storage Service (S3). Example:
640#. s3://other.host/bucket_name[/prefix]642#. s3://other.host/bucket_name[/prefix]
641#: ../duplicity/commandline.py:694643#: ../duplicity/commandline.py:712
642msgid "bucket_name"644msgid "bucket_name"
643msgstr ""645msgstr ""
644646
645#. Used in usage help to represent the name of a container in647#. Used in usage help to represent the name of a container in
646#. Amazon Web Services' Cloudfront. Example:648#. Amazon Web Services' Cloudfront. Example:
647#. cf+http://container_name649#. cf+http://container_name
648#: ../duplicity/commandline.py:705650#: ../duplicity/commandline.py:723
649msgid "container_name"651msgid "container_name"
650msgstr ""652msgstr ""
651653
652#. noun654#. noun
653#: ../duplicity/commandline.py:708655#: ../duplicity/commandline.py:726
654msgid "count"656msgid "count"
655msgstr ""657msgstr ""
656658
657#. Used in usage help to represent the name of a file directory659#. Used in usage help to represent the name of a file directory
658#: ../duplicity/commandline.py:711660#: ../duplicity/commandline.py:729
659msgid "directory"661msgid "directory"
660msgstr ""662msgstr ""
661663
662#. Used in usage help, e.g. to represent the name of a code664#. Used in usage help, e.g. to represent the name of a code
663#. module. Example:665#. module. Example:
664#. rsync://user[:password]@other.host[:port]::/module/some_dir666#. rsync://user[:password]@other.host[:port]::/module/some_dir
665#: ../duplicity/commandline.py:724667#: ../duplicity/commandline.py:742
666msgid "module"668msgid "module"
667msgstr ""669msgstr ""
668670
669#. Used in usage help to represent an internet hostname. Example:671#. Used in usage help to represent an internet hostname. Example:
670#. ftp://user[:password]@other.host[:port]/some_dir672#. ftp://user[:password]@other.host[:port]/some_dir
671#: ../duplicity/commandline.py:738673#: ../duplicity/commandline.py:756
672msgid "other.host"674msgid "other.host"
673msgstr ""675msgstr ""
674676
675#. Used in usage help. Example:677#. Used in usage help. Example:
676#. ftp://user[:password]@other.host[:port]/some_dir678#. ftp://user[:password]@other.host[:port]/some_dir
677#: ../duplicity/commandline.py:742679#: ../duplicity/commandline.py:760
678msgid "password"680msgid "password"
679msgstr ""681msgstr ""
680682
681#. Used in usage help to represent a TCP port number. Example:683#. Used in usage help to represent a TCP port number. Example:
682#. ftp://user[:password]@other.host[:port]/some_dir684#. ftp://user[:password]@other.host[:port]/some_dir
683#: ../duplicity/commandline.py:750685#: ../duplicity/commandline.py:768
684msgid "port"686msgid "port"
685msgstr ""687msgstr ""
686688
687#. Used in usage help. This represents a string to be used as a689#. Used in usage help. This represents a string to be used as a
688#. prefix to names for backup files created by Duplicity. Example:690#. prefix to names for backup files created by Duplicity. Example:
689#. s3://other.host/bucket_name[/prefix]691#. s3://other.host/bucket_name[/prefix]
690#: ../duplicity/commandline.py:755692#: ../duplicity/commandline.py:773
691msgid "prefix"693msgid "prefix"
692msgstr ""694msgstr ""
693695
694#. Used in usage help to represent a Unix-style path name. Example:696#. Used in usage help to represent a Unix-style path name. Example:
695#. rsync://user[:password]@other.host[:port]/relative_path697#. rsync://user[:password]@other.host[:port]/relative_path
696#: ../duplicity/commandline.py:759698#: ../duplicity/commandline.py:777
697msgid "relative_path"699msgid "relative_path"
698msgstr ""700msgstr ""
699701
700#. Used in usage help to represent the name of a single file702#. Used in usage help to represent the name of a single file
701#. directory or a Unix-style path to a directory. Example:703#. directory or a Unix-style path to a directory. Example:
702#. file:///some_dir704#. file:///some_dir
703#: ../duplicity/commandline.py:774705#: ../duplicity/commandline.py:792
704msgid "some_dir"706msgid "some_dir"
705msgstr ""707msgstr ""
706708
@@ -708,14 +710,14 @@
708#. directory or a Unix-style path to a directory where files will be710#. directory or a Unix-style path to a directory where files will be
709#. coming FROM. Example:711#. coming FROM. Example:
710#. duplicity [full|incremental] [options] source_dir target_url712#. duplicity [full|incremental] [options] source_dir target_url
711#: ../duplicity/commandline.py:780713#: ../duplicity/commandline.py:798
712msgid "source_dir"714msgid "source_dir"
713msgstr ""715msgstr ""
714716
715#. Used in usage help to represent a URL files will be coming717#. Used in usage help to represent a URL files will be coming
716#. FROM. Example:718#. FROM. Example:
717#. duplicity [restore] [options] source_url target_dir719#. duplicity [restore] [options] source_url target_dir
718#: ../duplicity/commandline.py:785720#: ../duplicity/commandline.py:803
719msgid "source_url"721msgid "source_url"
720msgstr ""722msgstr ""
721723
@@ -723,75 +725,75 @@
723#. directory or a Unix-style path to a directory. where files will be725#. directory or a Unix-style path to a directory. where files will be
724#. going TO. Example:726#. going TO. Example:
725#. duplicity [restore] [options] source_url target_dir727#. duplicity [restore] [options] source_url target_dir
726#: ../duplicity/commandline.py:791728#: ../duplicity/commandline.py:809
727msgid "target_dir"729msgid "target_dir"
728msgstr ""730msgstr ""
729731
730#. Used in usage help to represent a URL files will be going TO.732#. Used in usage help to represent a URL files will be going TO.
731#. Example:733#. Example:
732#. duplicity [full|incremental] [options] source_dir target_url734#. duplicity [full|incremental] [options] source_dir target_url
733#: ../duplicity/commandline.py:796735#: ../duplicity/commandline.py:814
734msgid "target_url"736msgid "target_url"
735msgstr ""737msgstr ""
736738
737#. Used in usage help to represent a user name (i.e. login).739#. Used in usage help to represent a user name (i.e. login).
738#. Example:740#. Example:
739#. ftp://user[:password]@other.host[:port]/some_dir741#. ftp://user[:password]@other.host[:port]/some_dir
740#: ../duplicity/commandline.py:806742#: ../duplicity/commandline.py:824
741msgid "user"743msgid "user"
742msgstr ""744msgstr ""
743745
744#. Header in usage help746#. Header in usage help
745#: ../duplicity/commandline.py:823747#: ../duplicity/commandline.py:841
746msgid "Backends and their URL formats:"748msgid "Backends and their URL formats:"
747msgstr ""749msgstr ""
748750
749#. Header in usage help751#. Header in usage help
750#: ../duplicity/commandline.py:848752#: ../duplicity/commandline.py:866
751msgid "Commands:"753msgid "Commands:"
752msgstr ""754msgstr ""
753755
754#: ../duplicity/commandline.py:872756#: ../duplicity/commandline.py:890
755#, python-format757#, python-format
756msgid "Specified archive directory '%s' does not exist, or is not a directory"758msgid "Specified archive directory '%s' does not exist, or is not a directory"
757msgstr ""759msgstr ""
758760
759#: ../duplicity/commandline.py:881761#: ../duplicity/commandline.py:899
760#, python-format762#, python-format
761msgid ""763msgid ""
762"Sign key should be an 8 character hex string, like 'AA0E73D2'.\n"764"Sign key should be an 8 character hex string, like 'AA0E73D2'.\n"
763"Received '%s' instead."765"Received '%s' instead."
764msgstr ""766msgstr ""
765767
766#: ../duplicity/commandline.py:941768#: ../duplicity/commandline.py:959
767#, python-format769#, python-format
768msgid ""770msgid ""
769"Restore destination directory %s already exists.\n"771"Restore destination directory %s already exists.\n"
770"Will not overwrite."772"Will not overwrite."
771msgstr ""773msgstr ""
772774
773#: ../duplicity/commandline.py:946775#: ../duplicity/commandline.py:964
774#, python-format776#, python-format
775msgid "Verify directory %s does not exist"777msgid "Verify directory %s does not exist"
776msgstr ""778msgstr ""
777779
778#: ../duplicity/commandline.py:952780#: ../duplicity/commandline.py:970
779#, python-format781#, python-format
780msgid "Backup source directory %s does not exist."782msgid "Backup source directory %s does not exist."
781msgstr ""783msgstr ""
782784
783#: ../duplicity/commandline.py:981785#: ../duplicity/commandline.py:999
784#, python-format786#, python-format
785msgid "Command line warning: %s"787msgid "Command line warning: %s"
786msgstr ""788msgstr ""
787789
788#: ../duplicity/commandline.py:981790#: ../duplicity/commandline.py:999
789msgid ""791msgid ""
790"Selection options --exclude/--include\n"792"Selection options --exclude/--include\n"
791"currently work only when backing up,not restoring."793"currently work only when backing up,not restoring."
792msgstr ""794msgstr ""
793795
794#: ../duplicity/commandline.py:1029796#: ../duplicity/commandline.py:1047
795#, python-format797#, python-format
796msgid ""798msgid ""
797"Bad URL '%s'.\n"799"Bad URL '%s'.\n"
@@ -799,61 +801,61 @@
799"\"file:///usr/local\". See the man page for more information."801"\"file:///usr/local\". See the man page for more information."
800msgstr ""802msgstr ""
801803
802#: ../duplicity/commandline.py:1054804#: ../duplicity/commandline.py:1072
803msgid "Main action: "805msgid "Main action: "
804msgstr ""806msgstr ""
805807
806#: ../duplicity/backend.py:87808#: ../duplicity/backend.py:109
807#, python-format809#, python-format
808msgid "Import of %s %s"810msgid "Import of %s %s"
809msgstr ""811msgstr ""
810812
811#: ../duplicity/backend.py:164813#: ../duplicity/backend.py:186
812#, python-format814#, python-format
813msgid "Could not initialize backend: %s"815msgid "Could not initialize backend: %s"
814msgstr ""816msgstr ""
815817
816#: ../duplicity/backend.py:320818#: ../duplicity/backend.py:311
817#, python-format819#, python-format
818msgid "Attempt %s failed: %s: %s"820msgid "Attempt %s failed: %s: %s"
819msgstr ""821msgstr ""
820822
821#: ../duplicity/backend.py:322 ../duplicity/backend.py:352823#: ../duplicity/backend.py:313 ../duplicity/backend.py:343
822#: ../duplicity/backend.py:359824#: ../duplicity/backend.py:350
823#, python-format825#, python-format
824msgid "Backtrace of previous error: %s"826msgid "Backtrace of previous error: %s"
825msgstr ""827msgstr ""
826828
827#: ../duplicity/backend.py:350829#: ../duplicity/backend.py:341
828#, python-format830#, python-format
829msgid "Attempt %s failed. %s: %s"831msgid "Attempt %s failed. %s: %s"
830msgstr ""832msgstr ""
831833
832#: ../duplicity/backend.py:361834#: ../duplicity/backend.py:352
833#, python-format835#, python-format
834msgid "Giving up after %s attempts. %s: %s"836msgid "Giving up after %s attempts. %s: %s"
835msgstr ""837msgstr ""
836838
837#: ../duplicity/backend.py:546 ../duplicity/backend.py:570839#: ../duplicity/backend.py:537 ../duplicity/backend.py:561
838#, python-format840#, python-format
839msgid "Reading results of '%s'"841msgid "Reading results of '%s'"
840msgstr ""842msgstr ""
841843
842#: ../duplicity/backend.py:585844#: ../duplicity/backend.py:576
843#, python-format845#, python-format
844msgid "Running '%s' failed with code %d (attempt #%d)"846msgid "Running '%s' failed with code %d (attempt #%d)"
845msgid_plural "Running '%s' failed with code %d (attempt #%d)"847msgid_plural "Running '%s' failed with code %d (attempt #%d)"
846msgstr[0] ""848msgstr[0] ""
847msgstr[1] ""849msgstr[1] ""
848850
849#: ../duplicity/backend.py:589851#: ../duplicity/backend.py:580
850#, python-format852#, python-format
851msgid ""853msgid ""
852"Error is:\n"854"Error is:\n"
853"%s"855"%s"
854msgstr ""856msgstr ""
855857
856#: ../duplicity/backend.py:591858#: ../duplicity/backend.py:582
857#, python-format859#, python-format
858msgid "Giving up trying to execute '%s' after %d attempt"860msgid "Giving up trying to execute '%s' after %d attempt"
859msgid_plural "Giving up trying to execute '%s' after %d attempts"861msgid_plural "Giving up trying to execute '%s' after %d attempts"
860862
=== modified file 'setup.py'
--- setup.py 2014-04-16 20:51:42 +0000
+++ setup.py 2014-04-16 20:51:42 +0000
@@ -28,8 +28,8 @@
2828
29version_string = "$version"29version_string = "$version"
3030
31if sys.version_info[:2] < (2,4):31if sys.version_info[:2] < (2, 6):
32 print "Sorry, duplicity requires version 2.4 or later of python"32 print "Sorry, duplicity requires version 2.6 or later of python"
33 sys.exit(1)33 sys.exit(1)
3434
35incdir_list = libdir_list = None35incdir_list = libdir_list = None
@@ -53,8 +53,6 @@
53 'README',53 'README',
54 'README-REPO',54 'README-REPO',
55 'README-LOG',55 'README-LOG',
56 'tarfile-LICENSE',
57 'tarfile-CHANGES',
58 'CHANGELOG']),56 'CHANGELOG']),
59 ]57 ]
6058
6159
=== removed file 'tarfile-CHANGES'
--- tarfile-CHANGES 2011-08-23 18:14:17 +0000
+++ tarfile-CHANGES 1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
1tarfile.py is a copy of python2.7's tarfile.py.
2
3No changes besides 2.4 compatibility have been made.
40
=== removed file 'tarfile-LICENSE'
--- tarfile-LICENSE 2011-10-05 14:13:31 +0000
+++ tarfile-LICENSE 1970-01-01 00:00:00 +0000
@@ -1,92 +0,0 @@
1irdu-backup uses tarfile, written by Lars Gustäbel. The following
2notice was included in the tarfile distribution:
3
4-----------------------------------------------------------------
5 tarfile - python module for accessing TAR archives
6
7 Lars Gustäbel <lars@gustaebel.de>
8-----------------------------------------------------------------
9
10
11Description
12-----------
13
14The tarfile module provides a set of functions for accessing TAR
15format archives. Because it is written in pure Python, it does
16not require any platform specific functions. GZIP compressed TAR
17archives are seamlessly supported.
18
19
20Requirements
21------------
22
23tarfile needs at least Python version 2.2.
24(For a tarfile for Python 1.5.2 take a look on the webpage.)
25
26
27!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
28IMPORTANT NOTE (*NIX only)
29--------------------------
30
31The addition of character and block devices is enabled by a C
32extension module (_tarfile.c), because Python does not yet
33provide the major() and minor() macros.
34Currently Linux and FreeBSD are implemented. If your OS is not
35supported, then please send me a patch.
36
37!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
38
39
40Download
41--------
42
43You can download the newest version at URL:
44http://www.gustaebel.de/lars/tarfile/
45
46
47Installation
48------------
49
501. extract the tarfile-x.x.x.tar.gz archive to a temporary folder
512. type "python setup.py install"
52
53
54Contact
55-------
56
57Suggestions, comments, bug reports and patches to:
58lars@gustaebel.de
59
60
61License
62-------
63
64Copyright (C) 2002 Lars Gustäbel <lars@gustaebel.de>
65All rights reserved.
66
67Permission is hereby granted, free of charge, to any person
68obtaining a copy of this software and associated documentation
69files (the "Software"), to deal in the Software without
70restriction, including without limitation the rights to use,
71copy, modify, merge, publish, distribute, sublicense, and/or sell
72copies of the Software, and to permit persons to whom the
73Software is furnished to do so, subject to the following
74conditions:
75
76The above copyright notice and this permission notice shall be
77included in all copies or substantial portions of the Software.
78
79THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
80EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
81OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
82NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
83HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
84WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
85FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
86OTHER DEALINGS IN THE SOFTWARE.
87
88
89README Version
90--------------
91
92$Id: tarfile-LICENSE,v 1.1 2002/10/29 01:49:46 bescoto Exp $
930
=== modified file 'testing/__init__.py'
--- testing/__init__.py 2014-04-16 20:51:42 +0000
+++ testing/__init__.py 2014-04-16 20:51:42 +0000
@@ -1,3 +0,0 @@
1import sys
2if sys.version_info < (2, 5,):
3 import tests
40
=== modified file 'testing/run-tests'
--- testing/run-tests 2014-04-16 20:51:42 +0000
+++ testing/run-tests 2014-04-16 20:51:42 +0000
@@ -46,7 +46,7 @@
46done46done
4747
48# run against all supported python versions48# run against all supported python versions
49for v in 2.4 2.5 2.6 2.7; do49for v in 2.6 2.7; do
50 type python$v >& /dev/null50 type python$v >& /dev/null
51 if [ $? == 1 ]; then51 if [ $? == 1 ]; then
52 echo "python$v not found on system"52 echo "python$v not found on system"
5353
=== modified file 'testing/run-tests-ve'
--- testing/run-tests-ve 2014-04-16 20:51:42 +0000
+++ testing/run-tests-ve 2014-04-16 20:51:42 +0000
@@ -46,7 +46,7 @@
46done46done
4747
48# run against all supported python versions48# run against all supported python versions
49for v in 2.4 2.5 2.6 2.7; do49for v in 2.6 2.7; do
50 ve=~/virtual$v50 ve=~/virtual$v
51 if [ $? == 1 ]; then51 if [ $? == 1 ]; then
52 echo "virtual$v not found on system"52 echo "virtual$v not found on system"
5353
=== modified file 'testing/tests/__init__.py'
--- testing/tests/__init__.py 2014-04-16 20:51:42 +0000
+++ testing/tests/__init__.py 2014-04-16 20:51:42 +0000
@@ -41,12 +41,3 @@
41# Standardize time41# Standardize time
42os.environ['TZ'] = 'US/Central'42os.environ['TZ'] = 'US/Central'
43time.tzset()43time.tzset()
44
45# Automatically add all submodules into this namespace. Helps python2.4
46# unittest work.
47if sys.version_info < (2, 5,):
48 for module in os.listdir(_this_dir):
49 if module == '__init__.py' or module[-3:] != '.py':
50 continue
51 __import__(module[:-3], locals(), globals())
52 del module
5344
=== modified file 'testing/tests/test_parsedurl.py'
--- testing/tests/test_parsedurl.py 2011-11-04 04:33:06 +0000
+++ testing/tests/test_parsedurl.py 2014-04-16 20:51:42 +0000
@@ -55,6 +55,13 @@
55 assert pu.username is None, pu.username55 assert pu.username is None, pu.username
56 assert pu.port is None, pu.port56 assert pu.port is None, pu.port
5757
58 pu = duplicity.backend.ParsedUrl("file://home")
59 assert pu.scheme == "file", pu.scheme
60 assert pu.netloc == "", pu.netloc
61 assert pu.path == "//home", pu.path
62 assert pu.username is None, pu.username
63 assert pu.port is None, pu.port
64
58 pu = duplicity.backend.ParsedUrl("ftp://foo@bar:pass@example.com:123/home")65 pu = duplicity.backend.ParsedUrl("ftp://foo@bar:pass@example.com:123/home")
59 assert pu.scheme == "ftp", pu.scheme66 assert pu.scheme == "ftp", pu.scheme
60 assert pu.netloc == "foo@bar:pass@example.com:123", pu.netloc67 assert pu.netloc == "foo@bar:pass@example.com:123", pu.netloc
@@ -121,7 +128,9 @@
121 def test_errors(self):128 def test_errors(self):
122 """Test various url errors"""129 """Test various url errors"""
123 self.assertRaises(InvalidBackendURL, duplicity.backend.ParsedUrl,130 self.assertRaises(InvalidBackendURL, duplicity.backend.ParsedUrl,
124 "ssh://foo@bar:pass@example.com:/home")131 "ssh:///home") # we require a hostname for ssh
132 self.assertRaises(InvalidBackendURL, duplicity.backend.ParsedUrl,
133 "file:path") # no relative paths for non-netloc schemes
125 self.assertRaises(UnsupportedBackendScheme, duplicity.backend.get_backend,134 self.assertRaises(UnsupportedBackendScheme, duplicity.backend.get_backend,
126 "foo://foo@bar:pass@example.com/home")135 "foo://foo@bar:pass@example.com/home")
127136
128137
=== modified file 'testing/tests/test_tarfile.py'
--- testing/tests/test_tarfile.py 2013-07-12 19:47:32 +0000
+++ testing/tests/test_tarfile.py 2014-04-16 20:51:42 +0000
@@ -1,7 +1,6 @@
1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2#2#
3# Copyright 2002 Ben Escoto <ben@emerose.org>3# Copyright 2013 Michael Terry <mike@mterry.name>
4# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
5#4#
6# This file is part of duplicity.5# This file is part of duplicity.
7#6#
@@ -19,309 +18,18 @@
19# along with duplicity; if not, write to the Free Software Foundation,18# along with duplicity; if not, write to the Free Software Foundation,
20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA19# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2120
22#
23# unittest for the tarfile module
24#
25# $Id: test_tarfile.py,v 1.11 2009/04/02 14:47:12 loafman Exp $
26
27import helper21import helper
28import sys, os, shutil, StringIO, tempfile, unittest, stat22import unittest
2923from duplicity import cached_ops
30from duplicity import tarfile24from duplicity import tarfile
3125
32helper.setup()26helper.setup()
3327
34SAMPLETAR = "testtar.tar"28
35TEMPDIR = tempfile.mktemp()29class TarfileTest(unittest.TestCase):
3630 def test_cached_ops(self):
37def join(*args):31 self.assertTrue(tarfile.grp is cached_ops)
38 return os.path.normpath(apply(os.path.join, args))32 self.assertTrue(tarfile.pwd is cached_ops)
39
40class BaseTest(unittest.TestCase):
41 """Base test for tarfile.
42 """
43
44 def setUp(self):
45 os.mkdir(TEMPDIR)
46 self.tar = tarfile.open(SAMPLETAR)
47 self.tar.errorlevel = 1
48
49 def tearDown(self):
50 self.tar.close()
51 shutil.rmtree(TEMPDIR)
52
53 def isroot(self):
54 return hasattr(os, "geteuid") and os.geteuid() == 0
55
56class Test_All(BaseTest):
57 """Allround test.
58 """
59 files_in_tempdir = ["tempdir",
60 "tempdir/0length",
61 "tempdir/large",
62 "tempdir/hardlinked1",
63 "tempdir/hardlinked2",
64 "tempdir/fifo",
65 "tempdir/symlink"]
66
67 tempdir_data = {"0length": "",
68 "large": "hello, world!" * 10000,
69 "hardlinked1": "foo",
70 "hardlinked2": "foo"}
71
72 def test_iteration(self):
73 """Test iteration through temp2.tar"""
74 self.make_temptar()
75 i = 0
76 tf = tarfile.TarFile("none", "r", FileLogger(open("temp2.tar", "rb")))
77 tf.debug = 3
78 for tarinfo in tf: i += 1 #@UnusedVariable
79 assert i >= 6, i
80
81 def _test_extraction(self):
82 """Test if regular files and links are extracted correctly.
83 """
84 for tarinfo in self.tar:
85 if tarinfo.isreg() or tarinfo.islnk() or tarinfo.issym():
86 self.tar.extract(tarinfo, TEMPDIR)
87 name = join(TEMPDIR, tarinfo.name)
88 data1 = file(name, "rb").read()
89 data2 = self.tar.extractfile(tarinfo).read()
90 self.assert_(data1 == data2,
91 "%s was not extracted successfully."
92 % tarinfo.name)
93
94 if not tarinfo.issym():
95 self.assert_(tarinfo.mtime == os.path.getmtime(name),
96 "%s's modification time was not set correctly."
97 % tarinfo.name)
98
99 if tarinfo.isdev():
100 if hasattr(os, "mkfifo") and tarinfo.isfifo():
101 self.tar.extract(tarinfo, TEMPDIR)
102 name = join(TEMPDIR, tarinfo.name)
103 self.assert_(tarinfo.mtime == os.path.getmtime(name),
104 "%s's modification time was not set correctly."
105 % tarinfo.name)
106
107 elif hasattr(os, "mknod") and self.isroot():
108 self.tar.extract(tarinfo, TEMPDIR)
109 name = join(TEMPDIR, tarinfo.name)
110 self.assert_(tarinfo.mtime == os.path.getmtime(name),
111 "%s's modification time was not set correctly."
112 % tarinfo.name)
113
114 def test_addition(self):
115 """Test if regular files are added correctly.
116 For this, we extract all regular files from our sample tar
117 and add them to a new one, which we check afterwards.
118 """
119 files = []
120 for tarinfo in self.tar:
121 if tarinfo.isreg():
122 self.tar.extract(tarinfo, TEMPDIR)
123 files.append(tarinfo.name)
124
125 buf = StringIO.StringIO()
126 tar = tarfile.open("test.tar", "w", buf)
127 for f in files:
128 path = join(TEMPDIR, f)
129 tarinfo = tar.gettarinfo(path)
130 tarinfo.name = f
131 tar.addfile(tarinfo, file(path, "rb"))
132 tar.close()
133
134 buf.seek(0)
135 tar = tarfile.open("test.tar", "r", buf)
136 for tarinfo in tar:
137 data1 = file(join(TEMPDIR, tarinfo.name), "rb").read()
138 data2 = tar.extractfile(tarinfo).read()
139 self.assert_(data1 == data2)
140 tar.close()
141
142 def make_tempdir(self):
143 """Make a temp directory with assorted files in it"""
144 try:
145 os.lstat("tempdir")
146 except OSError:
147 pass
148 else: # assume already exists
149 assert not os.system("rm -r tempdir")
150 os.mkdir("tempdir")
151
152 def write_file(name):
153 """Write appropriate data into file named name in tempdir"""
154 fp = open("tempdir/%s" % (name,), "wb")
155 fp.write(self.tempdir_data[name])
156 fp.close()
157
158 # Make 0length file
159 write_file("0length")
160 os.chmod("tempdir/%s" % ("0length",), 0604)
161
162 # Make regular file 130000 bytes in length
163 write_file("large")
164
165 # Make hard linked files
166 write_file("hardlinked1")
167 os.link("tempdir/hardlinked1", "tempdir/hardlinked2")
168
169 # Make a fifo
170 os.mkfifo("tempdir/fifo")
171
172 # Make symlink
173 os.symlink("foobar", "tempdir/symlink")
174
175 def make_temptar(self):
176 """Tar up tempdir, write to "temp2.tar" """
177 try:
178 os.lstat("temp2.tar")
179 except OSError:
180 pass
181 else:
182 assert not os.system("rm temp2.tar")
183
184 self.make_tempdir()
185 tf = tarfile.TarFile("temp2.tar", "w")
186 for filename in self.files_in_tempdir:
187 tf.add(filename, filename, 0)
188 tf.close()
189
190 def test_tarfile_creation(self):
191 """Create directory, make tarfile, extract using gnutar, compare"""
192 self.make_temptar()
193 self.extract_and_compare_tarfile()
194
195 def extract_and_compare_tarfile(self):
196 old_umask = os.umask(022)
197 os.system("rm -r tempdir")
198 assert not os.system("tar -xf temp2.tar")
199
200 def compare_data(name):
201 """Assert data is what should be"""
202 fp = open("tempdir/" + name, "rb")
203 buf = fp.read()
204 fp.close()
205 assert buf == self.tempdir_data[name]
206
207 s = os.lstat("tempdir")
208 assert stat.S_ISDIR(s.st_mode)
209
210 for key in self.tempdir_data: compare_data(key)
211
212 # Check to make sure permissions saved
213 s = os.lstat("tempdir/0length")
214 assert stat.S_IMODE(s.st_mode) == 0604, stat.S_IMODE(s.st_mode)
215
216 s = os.lstat("tempdir/fifo")
217 assert stat.S_ISFIFO(s.st_mode)
218
219 # Check to make sure hardlinked files still hardlinked
220 s1 = os.lstat("tempdir/hardlinked1")
221 s2 = os.lstat("tempdir/hardlinked2")
222 assert s1.st_ino == s2.st_ino
223
224 # Check symlink
225 s = os.lstat("tempdir/symlink")
226 assert stat.S_ISLNK(s.st_mode)
227
228 os.umask(old_umask)
229
230class Test_FObj(BaseTest):
231 """Test for read operations via file-object.
232 """
233
234 def _test_sparse(self):
235 """Test extraction of the sparse file.
236 """
237 BLOCK = 4096
238 for tarinfo in self.tar:
239 if tarinfo.issparse():
240 f = self.tar.extractfile(tarinfo)
241 b = 0
242 block = 0
243 while 1:
244 buf = f.read(BLOCK)
245 if not buf:
246 break
247 block += 1
248 self.assert_(BLOCK == len(buf))
249 if not b:
250 self.assert_("\0" * BLOCK == buf,
251 "sparse block is broken")
252 else:
253 self.assert_("0123456789ABCDEF" * 256 == buf,
254 "sparse block is broken")
255 b = 1 - b
256 self.assert_(block == 24, "too few sparse blocks")
257 f.close()
258
259 def _test_readlines(self):
260 """Test readlines() method of _FileObject.
261 """
262 self.tar.extract("pep.txt", TEMPDIR)
263 lines1 = file(join(TEMPDIR, "pep.txt"), "r").readlines()
264 lines2 = self.tar.extractfile("pep.txt").readlines()
265 self.assert_(lines1 == lines2, "readline() does not work correctly")
266
267 def _test_seek(self):
268 """Test seek() method of _FileObject, incl. random reading.
269 """
270 self.tar.extract("pep.txt", TEMPDIR)
271 data = file(join(TEMPDIR, "pep.txt"), "rb").read()
272
273 tarinfo = self.tar.getmember("pep.txt")
274 fobj = self.tar.extractfile(tarinfo)
275
276 text = fobj.read() #@UnusedVariable
277 fobj.seek(0)
278 self.assert_(0 == fobj.tell(),
279 "seek() to file's start failed")
280 fobj.seek(4096, 0)
281 self.assert_(4096 == fobj.tell(),
282 "seek() to absolute position failed")
283 fobj.seek(-2048, 1)
284 self.assert_(2048 == fobj.tell(),
285 "seek() to negative relative position failed")
286 fobj.seek(2048, 1)
287 self.assert_(4096 == fobj.tell(),
288 "seek() to positive relative position failed")
289 s = fobj.read(10)
290 self.assert_(s == data[4096:4106],
291 "read() after seek failed")
292 fobj.seek(0, 2)
293 self.assert_(tarinfo.size == fobj.tell(),
294 "seek() to file's end failed")
295 self.assert_(fobj.read() == "",
296 "read() at file's end did not return empty string")
297 fobj.seek(-tarinfo.size, 2)
298 self.assert_(0 == fobj.tell(),
299 "relative seek() to file's start failed")
300 fobj.seek(1024)
301 s1 = fobj.readlines()
302 fobj.seek(1024)
303 s2 = fobj.readlines()
304 self.assert_(s1 == s2,
305 "readlines() after seek failed")
306 fobj.close()
307
308class FileLogger:
309 """Like a file but log requests"""
310 def __init__(self, infp):
311 self.infp = infp
312 def read(self, length):
313 #print "Reading ", length
314 return self.infp.read(length)
315 def seek(self, position):
316 #print "Seeking to ", position
317 return self.infp.seek(position)
318 def tell(self):
319 #print "Telling"
320 return self.infp.tell()
321 def close(self):
322 #print "Closing"
323 return self.infp.close()
324
32533
326if __name__ == "__main__":34if __name__ == "__main__":
327 unittest.main()35 unittest.main()
32836
=== modified file 'testing/tests/test_unicode.py'
--- testing/tests/test_unicode.py 2013-12-27 06:39:00 +0000
+++ testing/tests/test_unicode.py 2014-04-16 20:51:42 +0000
@@ -29,13 +29,11 @@
29 if 'duplicity' in sys.modules:29 if 'duplicity' in sys.modules:
30 del(sys.modules["duplicity"])30 del(sys.modules["duplicity"])
3131
32 @patch('gettext.translation')32 @patch('gettext.install')
33 def test_module_install(self, gettext_mock):33 def test_module_install(self, gettext_mock):
34 """Make sure we convert translations to unicode"""34 """Make sure we convert translations to unicode"""
35 import duplicity35 import duplicity
36 gettext_mock.assert_called_once_with('duplicity', fallback=True)36 gettext_mock.assert_called_once_with('duplicity', unicode=True, names=['ngettext'])
37 gettext_mock.return_value.install.assert_called_once_with(unicode=True)
38 assert ngettext is gettext_mock.return_value.ungettext
3937
40if __name__ == "__main__":38if __name__ == "__main__":
41 unittest.main()39 unittest.main()
4240
=== removed file 'testing/testtar.tar'
43Binary files testing/testtar.tar 2002-10-29 01:49:46 +0000 and testing/testtar.tar 1970-01-01 00:00:00 +0000 differ41Binary files testing/testtar.tar 2002-10-29 01:49:46 +0000 and testing/testtar.tar 1970-01-01 00:00:00 +0000 differ

Subscribers

People subscribed via source and target branches

to all changes: