Merge lp:~kevinoid/duplicity/windows-port into lp:duplicity/0.6

Proposed by Kevin Locke
Status: Rejected
Rejected by: Kenneth Loafman
Proposed branch: lp:~kevinoid/duplicity/windows-port
Merge into: lp:duplicity/0.6
Diff against target: 1552 lines (+668/-245)
18 files modified
dist/makedist (+58/-35)
dist/setup.py (+9/-10)
duplicity-bin (+96/-17)
duplicity.1 (+5/-3)
duplicity/GnuPGInterface.py (+215/-127)
duplicity/backend.py (+14/-1)
duplicity/backends/localbackend.py (+10/-1)
duplicity/commandline.py (+14/-1)
duplicity/compilec.py (+16/-3)
duplicity/dup_temp.py (+9/-5)
duplicity/globals.py (+48/-5)
duplicity/manifest.py (+9/-3)
duplicity/patchdir.py (+15/-0)
duplicity/path.py (+102/-17)
duplicity/selection.py (+21/-11)
duplicity/tarfile.py (+6/-2)
po/update-pot (+0/-4)
po/update-pot.py (+21/-0)
To merge this branch: bzr merge lp:~kevinoid/duplicity/windows-port
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+39287@code.launchpad.net

Description of the change

This branch includes changes to support running Duplicity natively on Windows, as requested in bug 451582. I have done my best to separate out each change into logical units for commits and provide a detailed explanation and rationale for each change in the commit message.

The current work is only intended to port the main functionality of Duplicity and the local backend. The other backends have not been tested (and several, particularly ssh, are known not to work on Windows).

Most of the commits should not change any of the functionality of Duplicity. However, you may wish to take particular notice of revision 677 and 698 which do introduce functionality changes.

Note: Revision 682 added some portability improvements for the restart process and fixed bug 637556 in the process.

To post a comment you must log in.
Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote :

Rejecting for now. This needs to be reworked at this point to make it viable in the current codebase.

Unmerged revisions

703. By Kevin Locke <email address hidden>

Include updates to GnuPGInterface

These updates are from the current state of a branch of development that
I created to port GnuPGInterface to Windows, fix some bugs, and add
Python 3 support. The branch is available on github at
<http://github.com/kevinoid/py-gnupg>. These changes are taken from
commit 91667c.

I have assurances from the original author of GnuPGInterface that the
changes will be merged into his sources with minimal changes (if any)
once he has time and that it is safe to merge these changes into other
projects that need them without introducing a significant maintenance
burden.

Note: This version of GnuPGInterface now uses subprocess rather than
"raw" fork/exec. The threaded waitpid was removed due to threading
issues with subprocess (particularly Issue 1731717). It should be
largely unnecessary as any zombie child processes are reaped when a new
subprocess is started. If the more immediate reaping is required, the
threaded wait can easily be re-added (and less-easily be made thread
safe with subprocess).

Signed-off-by: Kevin Locke <email address hidden>

702. By Kevin Locke <email address hidden>

Unwrap temporary file instances

tempfile.TemporaryFile returns an instance of a wrapper class on
non-POSIX, non-Cygwin systems. Yet the librsync code requires a
standard file object. On Windows, the wrapper is unnecessary since the
file is opened with O_TEMPORARY and is deleted automatically on close.
On other systems the wrapper may be necessary to delete a file, so we
error out (in a way that future developers on those systems should be
able to find and understand...).

Note: This is a bit of a hack and relies on undocumented properties of
the object returned from tempfile.TemporaryFile. However, it avoids
the need to track and delete the temporary file ourselves.

Signed-off-by: Kevin Locke <email address hidden>

701. By Kevin Locke <email address hidden>

Make the manifest format more interoperable

At the cost of losing a bit of information, use the POSIX path
and EOL convention as part of the file format for manifest files.
This way backups created on one platform can be restored on another with
a different path and/or EOL convention.

To accomplish this, change the file mode to binary and convert native
paths to POSIX paths during manifest creation and back during load.

Note: During load the drive specifier for the target directory (if
there is one) is added to the POSIX-converted path. This allows
restoring cross-platform files more easily at the cost of losing a
warning about restoring files to a different drive than the original
backup. If this is unacceptable, the drive could be the first component
of the POSIX path or the cross-platform interoperability could be
dropped.

Signed-off-by: Kevin Locke <email address hidden>

700. By Kevin Locke <email address hidden>

Prevent Windows paths from being parsed as URLs

Windows paths which begin with a drive specifier are parsed as having a
scheme which is the drive letter. In order to avoid against these paths
being treated as URLs, check if the potential url_string looks like a
Windows path.

Note: The regex is not perfect, since it is possible that the "c" URL
scheme would not require slashes after the protocol. So limit the
checks to Windows platforms where paths are more likely to have this
form.

Signed-off-by: Kevin Locke <email address hidden>

699. By Kevin Locke <email address hidden>

Make file:// URL parsing more portable

Create path.from_url_path utility function which takes the path
component of a file URL and converts it into the native path
representation on the platform.

Note: RFC 1738 specifies that anything after file:// before the next
slash is a hostname (and therefore, implicitly, all paths are absolute).
However, to maintain backwards compatibility, allow for relative paths
to be specified in this manner.

Signed-off-by: Kevin Locke <email address hidden>

698. By Kevin Locke <email address hidden>

Ensure prefix ends with empty path component

WARNING: Functionality change

In order to prevent the given prefix from matching directories which
begin with the last component of prefix, ensure that prefix ends with an
empty path component (causing a tailing slash on POSIX systems).
Otherwise the prefix /foo/b would also include /foo/bar.

Note: If this functionality really was intended, wherever prefix is
removed from a path, the result should not start with a directory
separator (since it would result in a "/" first element in the return
value from path.split_all).

Signed-off-by: Kevin Locke <email address hidden>

697. By Kevin Locke <email address hidden>

Replace path manipulation based on "/"

Make use of path.split_all and os.path.join where paths were manipulated
based on "/"

In selection, change path import to keep path namespace, but alias
path.Path to Path to minimize the diff. The bare uses of split_all
seemed unnecessarily confusing and the other members of path are not
used directly so there is no real need for the * import.

Note: Removed unnecessary use of filter to remove blanks as this is
handled implicitly by os.path.split and therefore by path.split_all.

Note2: Need to be careful that os.path.join must have at least 1
argument, while "/".join() could work on a 0-length array.

Signed-off-by: Kevin Locke <email address hidden>

696. By Kevin Locke <email address hidden>

Create split_all utility function in path

This function is intended to replace the pathstr.split("/") idiom in
many places and provides a convenient way to break an arbitrary path
into its constituent components.

Note: Although it would be possible to split on os.altsep and os.sep
and it would work in many cases, and run a bit faster, it doesn't handle
more complex path systems and is likely to fail in corner cases even in
less complex path systems.

Signed-off-by: Kevin Locke <email address hidden>

695. By Kevin Locke <email address hidden>

Support systems without pwd and/or grp in tarfile

Inside tarfile this case is normally checked by the calling code, but
since uid2uname and gid2gname are called by Path without checking for
pwd and grp (and we want to support this case), remove the assertion
that these modules are defined. When they are not, throw KeyError
indicating the ID could not be mapped.

Signed-off-by: Kevin Locke <email address hidden>

694. By Kevin Locke <email address hidden>

Support more path systems in glob->regex conversion

 - Match against the system path separators, rather than "/"
 - Update duplicity.1 man page to indicate that the metacharacters match
   against the directory separator, rather than "/"

Note: This won't be portable to systems with more complex path systems
(like VMS), but that case is not trivial so it is worth waiting for a
need to arise.

Signed-off-by: Kevin Locke <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'dist/makedist'
--- dist/makedist 2010-07-22 19:15:11 +0000
+++ dist/makedist 2010-10-25 15:49:45 +0000
@@ -20,14 +20,14 @@
20# along with duplicity; if not, write to the Free Software Foundation,20# along with duplicity; if not, write to the Free Software Foundation,
21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2222
23import os, re, shutil, time, sys23import os, re, shutil, subprocess, tarfile, traceback, time, sys, zipfile
2424
25SourceDir = "duplicity"25SourceDir = "duplicity"
26DistDir = "dist"26DistDir = "dist"
2727
28# Various details about the files must also be specified by the rpm28# Various details about the files must also be specified by the rpm
29# spec template.29# spec template.
30spec_template = "dist/duplicity.spec.template"30spec_template = os.path.join("dist", "duplicity.spec.template")
3131
32def VersionedCopy(source, dest):32def VersionedCopy(source, dest):
33 """33 """
@@ -48,14 +48,13 @@
48 fout.write(outbuf)48 fout.write(outbuf)
49 assert not fout.close()49 assert not fout.close()
5050
51def MakeTar():51def MakeArchives():
52 """Create duplicity tar file"""52 """Create duplicity tar file"""
53 tardir = "duplicity-%s" % Version53 tardir = "duplicity-%s" % Version
54 tarfile = "duplicity-%s.tar.gz" % Version54 tarname = "duplicity-%s.tar.gz" % Version
55 try:55 zipname = "duplicity-%s.zip" % Version
56 os.lstat(tardir)56 if os.path.exists(tardir):
57 os.system("rm -rf " + tardir)57 shutil.rmtree(tardir)
58 except OSError: pass
5958
60 os.mkdir(tardir)59 os.mkdir(tardir)
61 for filename in [60 for filename in [
@@ -66,10 +65,10 @@
66 "LOG-README",65 "LOG-README",
67 "README",66 "README",
68 "tarfile-LICENSE",67 "tarfile-LICENSE",
69 SourceDir + "/_librsyncmodule.c",68 os.path.join(SourceDir, "_librsyncmodule.c"),
70 DistDir + "/setup.py",69 os.path.join(DistDir, "setup.py"),
71 ]:70 ]:
72 assert not os.system("cp %s %s" % (filename, tardir)), filename71 shutil.copy(filename, tardir)
7372
74 os.mkdir(os.path.join(tardir, "src"))73 os.mkdir(os.path.join(tardir, "src"))
75 for filename in [74 for filename in [
@@ -104,39 +103,46 @@
104 "urlparse_2_5.py",103 "urlparse_2_5.py",
105 "util.py",104 "util.py",
106 ]:105 ]:
107 assert not os.system("cp %s/%s %s/src" % (SourceDir, filename, tardir)), filename106 shutil.copy(os.path.join(SourceDir, filename),
107 os.path.join(tardir, "src"))
108108
109 os.mkdir(os.path.join(tardir, "src", "backends"))109 os.mkdir(os.path.join(tardir, "src", "backends"))
110 for filename in [110 for filename in [
111 "backends/botobackend.py",111 "botobackend.py",
112 "backends/cloudfilesbackend.py",112 "cloudfilesbackend.py",
113 "backends/ftpbackend.py",113 "ftpbackend.py",
114 "backends/giobackend.py",114 "giobackend.py",
115 "backends/hsibackend.py",115 "hsibackend.py",
116 "backends/imapbackend.py",116 "imapbackend.py",
117 "backends/__init__.py",117 "__init__.py",
118 "backends/localbackend.py",118 "localbackend.py",
119 "backends/rsyncbackend.py",119 "rsyncbackend.py",
120 "backends/sshbackend.py",120 "sshbackend.py",
121 "backends/tahoebackend.py",121 "tahoebackend.py",
122 "backends/webdavbackend.py",122 "webdavbackend.py",
123 ]:123 ]:
124 assert not os.system("cp %s/%s %s/src/backends" %124 shutil.copy(os.path.join(SourceDir, "backends", filename),
125 (SourceDir, filename, tardir)), filename125 os.path.join(tardir, "src", "backends"))
126
127 if subprocess.call([sys.executable, "update-pot.py"], cwd="po") != 0:
128 sys.stderr.write("update-pot.py failed, translation files not updated!\n")
126129
127 os.mkdir(os.path.join(tardir, "po"))130 os.mkdir(os.path.join(tardir, "po"))
128 assert not os.system("cd po && ./update-pot")
129 for filename in [131 for filename in [
130 "duplicity.pot",132 "duplicity.pot",
131 ]:133 ]:
132 assert not os.system("cp po/%s %s/po" % (filename, tardir)), filename134 shutil.copy(os.path.join("po", filename), os.path.join(tardir, "po"))
133 linguas = open('po/LINGUAS')135 linguas = open(os.path.join("po", "LINGUAS"))
134 for line in linguas:136 for line in linguas:
135 langs = line.split()137 langs = line.split()
136 for lang in langs:138 for lang in langs:
137 assert not os.mkdir(os.path.join(tardir, "po", lang)), lang139 assert not os.mkdir(os.path.join(tardir, "po", lang)), lang
138 assert not os.system("cp po/%s.po %s/po/%s" % (lang, tardir, lang)), lang140 shutil.copy(os.path.join("po", lang + ".po"),
139 assert not os.system("msgfmt po/%s.po -o %s/po/%s/duplicity.mo" % (lang, tardir, lang)), lang141 os.path.join(tardir, "po", lang))
142 if os.system("msgfmt %s -o %s" %
143 (os.path.join("po", lang + ".po"),
144 os.path.join(tardir, "po", lang, "duplicity.mo"))) != 0:
145 sys.stderr.write("Translation for " + lang + " NOT updated!\n")
140 linguas.close()146 linguas.close()
141147
142 VersionedCopy(os.path.join(SourceDir, "globals.py"),148 VersionedCopy(os.path.join(SourceDir, "globals.py"),
@@ -154,9 +160,26 @@
154160
155 os.chmod(os.path.join(tardir, "setup.py"), 0755)161 os.chmod(os.path.join(tardir, "setup.py"), 0755)
156 os.chmod(os.path.join(tardir, "rdiffdir"), 0644)162 os.chmod(os.path.join(tardir, "rdiffdir"), 0644)
157 os.system("tar -czf %s %s" % (tarfile, tardir))163
164 with tarfile.open(tarname, "w:gz") as tar:
165 tar.add(tardir)
166
167 def add_dir_to_zip(dir, zip, clen=None):
168 if clen == None:
169 clen = len(dir)
170
171 for entry in os.listdir(dir):
172 entrypath = os.path.join(dir, entry)
173 if os.path.isdir(entrypath):
174 add_dir_to_zip(entrypath, zip, clen)
175 else:
176 zip.write(entrypath, entrypath[clen:])
177
178 with zipfile.ZipFile(zipname, "w") as zip:
179 add_dir_to_zip(tardir, zip)
180
158 shutil.rmtree(tardir)181 shutil.rmtree(tardir)
159 return tarfile182 return (tarname, zipname)
160183
161def MakeSpecFile():184def MakeSpecFile():
162 """Create spec file using spec template"""185 """Create spec file using spec template"""
@@ -166,8 +189,8 @@
166189
167def Main():190def Main():
168 print "Processing version " + Version191 print "Processing version " + Version
169 tarfile = MakeTar()192 archives = MakeArchives()
170 print "Made tar file " + tarfile193 print "Made archives: %s" % (archives,)
171 specfile = MakeSpecFile()194 specfile = MakeSpecFile()
172 print "Made specfile " + specfile195 print "Made specfile " + specfile
173196
174197
=== modified file 'dist/setup.py'
--- dist/setup.py 2010-10-06 14:37:22 +0000
+++ dist/setup.py 2010-10-25 15:49:45 +0000
@@ -31,16 +31,15 @@
3131
32incdir_list = libdir_list = None32incdir_list = libdir_list = None
3333
34if os.name == 'posix':34LIBRSYNC_DIR = os.environ.get('LIBRSYNC_DIR', '')
35 LIBRSYNC_DIR = os.environ.get('LIBRSYNC_DIR', '')35args = sys.argv[:]
36 args = sys.argv[:]36for arg in args:
37 for arg in args:37 if arg.startswith('--librsync-dir='):
38 if arg.startswith('--librsync-dir='):38 LIBRSYNC_DIR = arg.split('=')[1]
39 LIBRSYNC_DIR = arg.split('=')[1]39 sys.argv.remove(arg)
40 sys.argv.remove(arg)40if LIBRSYNC_DIR:
41 if LIBRSYNC_DIR:41 incdir_list = [os.path.join(LIBRSYNC_DIR, 'include')]
42 incdir_list = [os.path.join(LIBRSYNC_DIR, 'include')]42 libdir_list = [os.path.join(LIBRSYNC_DIR, 'lib')]
43 libdir_list = [os.path.join(LIBRSYNC_DIR, 'lib')]
4443
45data_files = [('share/man/man1',44data_files = [('share/man/man1',
46 ['duplicity.1',45 ['duplicity.1',
4746
=== modified file 'duplicity-bin'
--- duplicity-bin 2010-08-26 13:01:10 +0000
+++ duplicity-bin 2010-10-25 15:49:45 +0000
@@ -28,11 +28,24 @@
28# any suggestions.28# any suggestions.
2929
30import getpass, gzip, os, sys, time, types30import getpass, gzip, os, sys, time, types
31import traceback, platform, statvfs, resource, re31import traceback, platform, statvfs, re
3232
33import gettext33import gettext
34gettext.install('duplicity')34gettext.install('duplicity')
3535
36try:
37 import resource
38 have_resource = True
39except ImportError:
40 have_resource = False
41
42if sys.platform == "win32":
43 import ctypes
44 import ctypes.util
45 # Not to be confused with Python's msvcrt module which wraps part of msvcrt
46 # Note: Load same msvcrt as Python to avoid cross-CRT problems
47 ctmsvcrt = ctypes.cdll[ctypes.util.find_msvcrt()]
48
36from duplicity import log49from duplicity import log
37log.setup()50log.setup()
3851
@@ -554,7 +567,10 @@
554 @param col_stats: collection status567 @param col_stats: collection status
555 """568 """
556 if globals.restore_dir:569 if globals.restore_dir:
557 index = tuple(globals.restore_dir.split("/"))570 index = path.split_all(globals.restore_dir)
571 if index[-1] == "":
572 del index[-1]
573 index = tuple(index)
558 else:574 else:
559 index = ()575 index = ()
560 time = globals.restore_time or dup_time.curtime576 time = globals.restore_time or dup_time.curtime
@@ -994,16 +1010,13 @@
994 # First check disk space in temp area.1010 # First check disk space in temp area.
995 tempfile, tempname = tempdir.default().mkstemp()1011 tempfile, tempname = tempdir.default().mkstemp()
996 os.close(tempfile)1012 os.close(tempfile)
1013
997 # strip off the temp dir and file1014 # strip off the temp dir and file
998 tempfs = os.path.sep.join(tempname.split(os.path.sep)[:-2])1015 tempfs = os.path.split(os.path.split(tempname)[0])[0]
999 try:1016
1000 stats = os.statvfs(tempfs)
1001 except:
1002 log.FatalError(_("Unable to get free space on temp."),
1003 log.ErrorCode.get_freespace_failed)
1004 # Calculate space we need for at least 2 volumes of full or inc1017 # Calculate space we need for at least 2 volumes of full or inc
1005 # plus about 30% of one volume for the signature files.1018 # plus about 30% of one volume for the signature files.
1006 freespace = stats[statvfs.F_FRSIZE] * stats[statvfs.F_BAVAIL]1019 freespace = get_free_space(tempfs)
1007 needspace = (((globals.async_concurrency + 1) * globals.volsize)1020 needspace = (((globals.async_concurrency + 1) * globals.volsize)
1008 + int(0.30 * globals.volsize))1021 + int(0.30 * globals.volsize))
1009 if freespace < needspace:1022 if freespace < needspace:
@@ -1015,16 +1028,82 @@
10151028
1016 # Some environments like Cygwin run with an artificially1029 # Some environments like Cygwin run with an artificially
1017 # low value for max open files. Check for safe number.1030 # low value for max open files. Check for safe number.
1031 check_resource_limits()
1032
1033
1034def check_resource_limits():
1035 """
1036 Check for sufficient resource limits:
1037 - enough max open files
1038 Attempt to increase limits to sufficient values if insufficient
1039 Put out fatal error if not sufficient to run
1040
1041 Requires the resource module
1042
1043 @rtype: void
1044 @return: void
1045 """
1046 if have_resource:
1018 try:1047 try:
1019 soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)1048 soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
1020 except resource.error:1049 except resource.error:
1021 log.FatalError(_("Unable to get max open files."),1050 log.FatalError(_("Unable to get max open files."),
1022 log.ErrorCode.get_ulimit_failed)1051 log.ErrorCode.get_ulimit_failed)
1023 maxopen = min([l for l in (soft, hard) if l > -1])1052
1024 if maxopen < 1024:1053 if soft > -1 and soft < 1024 and soft < hard:
1025 log.FatalError(_("Max open files of %s is too low, should be >= 1024.\n"1054 try:
1026 "Use 'ulimit -n 1024' or higher to correct.\n") % (maxopen,),1055 newsoft = min(1024, hard)
1027 log.ErrorCode.maxopen_too_low)1056 resource.setrlimit(resource.RLIMIT_NOFILE, (newsoft, hard))
1057 soft = newsoft
1058 except resource.error:
1059 pass
1060 elif sys.platform == "win32":
1061 # 2048 from http://msdn.microsoft.com/en-us/library/6e3b887c.aspx
1062 soft, hard = ctmsvcrt._getmaxstdio(), 2048
1063
1064 if soft < 1024:
1065 newsoft = ctmsvcrt._setmaxstdio(1024)
1066 if newsoft > -1:
1067 soft = newsoft
1068 else:
1069 log.FatalError(_("Unable to get max open files."),
1070 log.ErrorCode.get_ulimit_failed)
1071
1072 maxopen = min([l for l in (soft, hard) if l > -1])
1073 if maxopen < 1024:
1074 log.FatalError(_("Max open files of %s is too low, should be >= 1024.\n"
1075 "Use 'ulimit -n 1024' or higher to correct.\n") % (maxopen,),
1076 log.ErrorCode.maxopen_too_low)
1077
1078
1079def get_free_space(dir):
1080 """
1081 Get the free space available in a given directory
1082
1083 @type dir: string
1084 @param dir: directory in which to measure free space
1085
1086 @rtype: int
1087 @return: amount of free space on the filesystem containing dir (in bytes)
1088 """
1089 if hasattr(os, "statvfs"):
1090 try:
1091 stats = os.statvfs(dir)
1092 except:
1093 log.FatalError(_("Unable to get free space on temp."),
1094 log.ErrorCode.get_freespace_failed)
1095
1096 return stats[statvfs.F_FRSIZE] * stats[statvfs.F_BAVAIL]
1097 elif sys.platform == "win32":
1098 freespaceull = ctypes.c_ulonglong(0)
1099 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(dir),
1100 None, None, ctypes.pointer(freespaceull))
1101
1102 return freespaceull.value
1103 else:
1104 log.FatalError(_("Unable to get free space on temp."),
1105 log.ErrorCode.get_freespace_failed)
1106
10281107
1029def log_startup_parms(verbosity=log.INFO):1108def log_startup_parms(verbosity=log.INFO):
1030 """1109 """
@@ -1071,7 +1150,7 @@
1071 log.Notice(_("RESTART: The first volume failed to upload before termination.\n"1150 log.Notice(_("RESTART: The first volume failed to upload before termination.\n"
1072 " Restart is impossible...starting backup from beginning."))1151 " Restart is impossible...starting backup from beginning."))
1073 self.last_backup.delete()1152 self.last_backup.delete()
1074 os.execve(sys.argv[0], sys.argv[1:], os.environ)1153 self.execv(sys.executable, sys.argv)
1075 elif mf_len - self.start_vol > 0:1154 elif mf_len - self.start_vol > 0:
1076 # upload of N vols failed, fix manifest and restart1155 # upload of N vols failed, fix manifest and restart
1077 log.Notice(_("RESTART: Volumes %d to %d failed to upload before termination.\n"1156 log.Notice(_("RESTART: Volumes %d to %d failed to upload before termination.\n"
@@ -1087,7 +1166,7 @@
1087 " backup then restart the backup from the beginning.") %1166 " backup then restart the backup from the beginning.") %
1088 (mf_len, self.start_vol))1167 (mf_len, self.start_vol))
1089 self.last_backup.delete()1168 self.last_backup.delete()
1090 os.execve(sys.argv[0], sys.argv[1:], os.environ)1169 self.execv(sys.executable, sys.argv)
10911170
10921171
1093 def setLastSaved(self, mf):1172 def setLastSaved(self, mf):
@@ -1112,7 +1191,7 @@
11121191
1113 # if python is run setuid, it's only partway set,1192 # if python is run setuid, it's only partway set,
1114 # so make sure to run with euid/egid of root1193 # so make sure to run with euid/egid of root
1115 if os.geteuid() == 0:1194 if hasattr(os, "geteuid") and os.geteuid() == 0:
1116 # make sure uid/gid match euid/egid1195 # make sure uid/gid match euid/egid
1117 os.setuid(os.geteuid())1196 os.setuid(os.geteuid())
1118 os.setgid(os.getegid())1197 os.setgid(os.getegid())
11191198
=== modified file 'duplicity.1'
--- duplicity.1 2010-08-26 14:11:14 +0000
+++ duplicity.1 2010-10-25 15:49:45 +0000
@@ -871,14 +871,16 @@
871.BR [...] .871.BR [...] .
872As in a normal shell,872As in a normal shell,
873.B *873.B *
874can be expanded to any string of characters not containing "/",874can be expanded to any string of characters not containing a directory
875separator,
875.B ?876.B ?
876expands to any character except "/", and877expands to any character except a directory separator, and
877.B [...]878.B [...]
878expands to a single character of those characters specified (ranges879expands to a single character of those characters specified (ranges
879are acceptable). The new special pattern,880are acceptable). The new special pattern,
880.BR ** ,881.BR ** ,
881expands to any string of characters whether or not it contains "/".882expands to any string of characters whether or not it contains a
883directory separator.
882Furthermore, if the pattern starts with "ignorecase:" (case884Furthermore, if the pattern starts with "ignorecase:" (case
883insensitive), then this prefix will be removed and any character in885insensitive), then this prefix will be removed and any character in
884the string can be replaced with an upper- or lowercase version of886the string can be replaced with an upper- or lowercase version of
885887
=== modified file 'duplicity/GnuPGInterface.py'
--- duplicity/GnuPGInterface.py 2010-07-22 19:15:11 +0000
+++ duplicity/GnuPGInterface.py 2010-10-25 15:49:45 +0000
@@ -220,42 +220,55 @@
220or see http://www.gnu.org/copyleft/lesser.html220or see http://www.gnu.org/copyleft/lesser.html
221"""221"""
222222
223import errno
223import os224import os
225import subprocess
224import sys226import sys
225import fcntl227
226228if sys.platform == "win32":
227from duplicity import log229 # Required windows-only imports
228230 import msvcrt
229try:231 import _subprocess
230 import threading232
233# Define next function for Python pre-2.6
234try:
235 next
236except NameError:
237 def next(itr):
238 return itr.next()
239
240try:
241 import fcntl
231except ImportError:242except ImportError:
232 import dummy_threading #@UnusedImport243 # import success/failure is checked before use
233 log.Warn("Threading not available -- zombie processes may appear")244 pass
234245
235__author__ = "Frank J. Tobin, ftobin@neverending.org"246__author__ = "Frank J. Tobin, ftobin@neverending.org"
236__version__ = "0.3.2"247__version__ = "0.3.2"
237__revision__ = "$Id: GnuPGInterface.py,v 1.6 2009/06/06 17:35:19 loafman Exp $"248__revision__ = "$Id$"
238249
239# "standard" filehandles attached to processes250# "standard" filehandles attached to processes
240_stds = [ 'stdin', 'stdout', 'stderr' ]251_stds = [ 'stdin', 'stdout', 'stderr' ]
241252
242# the permissions each type of fh needs to be opened with253# the permissions each type of fh needs to be opened with
243_fd_modes = { 'stdin': 'w',254_fd_modes = { 'stdin': 'wb',
244 'stdout': 'r',255 'stdout': 'rb',
245 'stderr': 'r',256 'stderr': 'rb',
246 'passphrase': 'w',257 'passphrase': 'wb',
247 'command': 'w',258 'attribute': 'rb',
248 'logger': 'r',259 'command': 'wb',
249 'status': 'r'260 'logger': 'rb',
261 'status': 'rb'
250 }262 }
251263
252# correlation between handle names and the arguments we'll pass264# correlation between handle names and the arguments we'll pass
253_fd_options = { 'passphrase': '--passphrase-fd',265_fd_options = { 'passphrase': '--passphrase-fd',
254 'logger': '--logger-fd',266 'logger': '--logger-fd',
255 'status': '--status-fd',267 'status': '--status-fd',
256 'command': '--command-fd' }268 'command': '--command-fd',
269 'attribute': '--attribute-fd' }
257270
258class GnuPG:271class GnuPG(object):
259 """Class instances represent GnuPG.272 """Class instances represent GnuPG.
260273
261 Instance attributes of a GnuPG object are:274 Instance attributes of a GnuPG object are:
@@ -276,6 +289,8 @@
276 the command-line options used when calling GnuPG.289 the command-line options used when calling GnuPG.
277 """290 """
278291
292 __slots__ = ['call', 'passphrase', 'options']
293
279 def __init__(self):294 def __init__(self):
280 self.call = 'gpg'295 self.call = 'gpg'
281 self.passphrase = None296 self.passphrase = None
@@ -349,14 +364,14 @@
349 if attach_fhs == None: attach_fhs = {}364 if attach_fhs == None: attach_fhs = {}
350365
351 for std in _stds:366 for std in _stds:
352 if not attach_fhs.has_key(std) \367 if std not in attach_fhs \
353 and std not in create_fhs:368 and std not in create_fhs:
354 attach_fhs.setdefault(std, getattr(sys, std))369 attach_fhs.setdefault(std, getattr(sys, std))
355370
356 handle_passphrase = 0371 handle_passphrase = 0
357372
358 if self.passphrase != None \373 if self.passphrase != None \
359 and not attach_fhs.has_key('passphrase') \374 and 'passphrase' not in attach_fhs \
360 and 'passphrase' not in create_fhs:375 and 'passphrase' not in create_fhs:
361 handle_passphrase = 1376 handle_passphrase = 1
362 create_fhs.append('passphrase')377 create_fhs.append('passphrase')
@@ -366,7 +381,10 @@
366381
367 if handle_passphrase:382 if handle_passphrase:
368 passphrase_fh = process.handles['passphrase']383 passphrase_fh = process.handles['passphrase']
369 passphrase_fh.write( self.passphrase )384 if sys.version_info >= (3, 0) and isinstance(self.passphrase, str):
385 passphrase_fh.write( self.passphrase.encode() )
386 else:
387 passphrase_fh.write( self.passphrase )
370 passphrase_fh.close()388 passphrase_fh.close()
371 del process.handles['passphrase']389 del process.handles['passphrase']
372390
@@ -379,45 +397,40 @@
379397
380 process = Process()398 process = Process()
381399
382 for fh_name in create_fhs + attach_fhs.keys():400 for fh_name in create_fhs + list(attach_fhs.keys()):
383 if not _fd_modes.has_key(fh_name):401 if fh_name not in _fd_modes:
384 raise KeyError, \402 raise KeyError("unrecognized filehandle name '%s'; must be one of %s" \
385 "unrecognized filehandle name '%s'; must be one of %s" \403 % (fh_name, list(_fd_modes.keys())))
386 % (fh_name, _fd_modes.keys())
387404
388 for fh_name in create_fhs:405 for fh_name in create_fhs:
389 # make sure the user doesn't specify a filehandle406 # make sure the user doesn't specify a filehandle
390 # to be created *and* attached407 # to be created *and* attached
391 if attach_fhs.has_key(fh_name):408 if fh_name in attach_fhs:
392 raise ValueError, \409 raise ValueError("cannot have filehandle '%s' in both create_fhs and attach_fhs" \
393 "cannot have filehandle '%s' in both create_fhs and attach_fhs" \410 % fh_name)
394 % fh_name
395411
396 pipe = os.pipe()412 pipe = os.pipe()
397 # fix by drt@un.bewaff.net noting413 # fix by drt@un.bewaff.net noting
398 # that since pipes are unidirectional on some systems,414 # that since pipes are unidirectional on some systems,
399 # so we have to 'turn the pipe around'415 # so we have to 'turn the pipe around'
400 # if we are writing416 # if we are writing
401 if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0])417 if _fd_modes[fh_name][0] == 'w': pipe = (pipe[1], pipe[0])
418
419 # Close the parent end in child to prevent deadlock
420 if "fcntl" in globals():
421 fcntl.fcntl(pipe[0], fcntl.F_SETFD, fcntl.FD_CLOEXEC)
422
402 process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)423 process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0)
403424
404 for fh_name, fh in attach_fhs.items():425 for fh_name, fh in attach_fhs.items():
405 process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)426 process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1)
406427
407 process.pid = os.fork()428 self._launch_process(process, gnupg_commands, args)
408 if process.pid != 0:429 return self._handle_pipes(process)
409 # start a threaded_waitpid on the child430
410 process.thread = threading.Thread(target=threaded_waitpid,431
411 name="wait%d" % process.pid,432 def _handle_pipes(self, process):
412 args=(process,))433 """Deal with pipes after the child process has been created"""
413 process.thread.start()
414
415 if process.pid == 0: self._as_child(process, gnupg_commands, args)
416 return self._as_parent(process)
417
418
419 def _as_parent(self, process):
420 """Stuff run after forking in parent"""
421 for k, p in process._pipes.items():434 for k, p in process._pipes.items():
422 if not p.direct:435 if not p.direct:
423 os.close(p.child)436 os.close(p.child)
@@ -428,43 +441,137 @@
428441
429 return process442 return process
430443
431444 def _create_preexec_fn(self, process):
432 def _as_child(self, process, gnupg_commands, args):445 """Create and return a function to do cleanup before exec
433 """Stuff run after forking in child"""446
434 # child447 The cleanup function will close all file descriptors which are not
435 for std in _stds:448 needed by the child process. This is required to prevent unnecessary
436 p = process._pipes[std]449 blocking on the final read of pipes not set FD_CLOEXEC due to gpg
437 os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() )450 inheriting an open copy of the input end of the pipe. This can cause
438451 delays in unrelated parts of the program or deadlocks in the case that
439 for k, p in process._pipes.items():452 one end of the pipe is passed to attach_fds.
440 if p.direct and k not in _stds:453
441 # we want the fh to stay open after execing454 FIXME: There is a race condition where a pipe can be created in
442 fcntl.fcntl( p.child, fcntl.F_SETFD, 0 )455 another thread after this function runs before exec is called and it
443456 will not be closed. This race condition will remain until a better
457 way to avoid closing the error pipe created by submodule is identified.
458 """
459 if sys.platform == "win32":
460 return None # No cleanup necessary
461
462 try:
463 MAXFD = os.sysconf("SC_OPEN_MAX")
464 if MAXFD == -1:
465 MAXFD = 256
466 except:
467 MAXFD = 256
468
469 # Get list of fds to close now, so we don't close the error pipe
470 # created by submodule for reporting exec errors
471 child_fds = [p.child for p in process._pipes.values()]
472 child_fds.sort()
473 child_fds.append(MAXFD) # Sentinel value, simplifies code greatly
474
475 child_fds_iter = iter(child_fds)
476 child_fd = next(child_fds_iter)
477 while child_fd < 3:
478 child_fd = next(child_fds_iter)
479
480 extra_fds = []
481 # FIXME: Is there a better (portable) way to list all open FDs?
482 for fd in range(3, MAXFD):
483 if fd > child_fd:
484 child_fd = next(child_fds_iter)
485
486 if fd == child_fd:
487 continue
488
489 try:
490 # Note: Can't use lseek, can cause nul byte in pipes
491 # where the position has not been set by read/write
492 #os.lseek(fd, os.SEEK_CUR, 0)
493 os.tcgetpgrp(fd)
494 except OSError:
495 # FIXME: When support for Python 2.5 is dropped, use 'as'
496 oe = sys.exc_info()[1]
497 if oe.errno == errno.EBADF:
498 continue
499
500 extra_fds.append(fd)
501
502 def preexec_fn():
503 # Note: This function runs after standard FDs have been renumbered
504 # from their original values to 0, 1, 2
505
506 for fd in extra_fds:
507 try:
508 os.close(fd)
509 except OSError:
510 pass
511
512 # Ensure that all descriptors passed to the child will remain open
513 # Arguably FD_CLOEXEC descriptors should be an argument error
514 # But for backwards compatibility, we just fix it here (after fork)
515 for fd in [0, 1, 2] + child_fds[:-1]:
516 try:
517 fcntl.fcntl(fd, fcntl.F_SETFD, 0)
518 except OSError:
519 # Will happen for renumbered FDs
520 pass
521
522 return preexec_fn
523
524
525 def _launch_process(self, process, gnupg_commands, args):
526 """Run the child process"""
444 fd_args = []527 fd_args = []
445
446 for k, p in process._pipes.items():528 for k, p in process._pipes.items():
447 # set command-line options for non-standard fds529 # set command-line options for non-standard fds
448 if k not in _stds:530 if k in _stds:
449 fd_args.extend([ _fd_options[k], "%d" % p.child ])531 continue
450532
451 if not p.direct: os.close(p.parent)533 if sys.platform == "win32":
534 # Must pass inheritable os file handle
535 curproc = _subprocess.GetCurrentProcess()
536 pchandle = msvcrt.get_osfhandle(p.child)
537 pcihandle = _subprocess.DuplicateHandle(
538 curproc, pchandle, curproc, 0, 1,
539 _subprocess.DUPLICATE_SAME_ACCESS)
540 fdarg = pcihandle.Detach()
541 else:
542 # Must pass file descriptor
543 fdarg = p.child
544 fd_args.extend([ _fd_options[k], str(fdarg) ])
452545
453 command = [ self.call ] + fd_args + self.options.get_args() \546 command = [ self.call ] + fd_args + self.options.get_args() \
454 + gnupg_commands + args547 + gnupg_commands + args
455548
456 os.execvp( command[0], command )549 if len(fd_args) > 0:
457550 # Can't close all file descriptors
458551 # Create preexec function to close what we can
459class Pipe:552 preexec_fn = self._create_preexec_fn(process)
553
554 process._subproc = subprocess.Popen(command,
555 stdin=process._pipes['stdin'].child,
556 stdout=process._pipes['stdout'].child,
557 stderr=process._pipes['stderr'].child,
558 close_fds=not len(fd_args) > 0,
559 preexec_fn=preexec_fn,
560 shell=False)
561 process.pid = process._subproc.pid
562
563
564class Pipe(object):
460 """simple struct holding stuff about pipes we use"""565 """simple struct holding stuff about pipes we use"""
566 __slots__ = ['parent', 'child', 'direct']
567
461 def __init__(self, parent, child, direct):568 def __init__(self, parent, child, direct):
462 self.parent = parent569 self.parent = parent
463 self.child = child570 self.child = child
464 self.direct = direct571 self.direct = direct
465572
466573
467class Options:574class Options(object):
468 """Objects of this class encompass options passed to GnuPG.575 """Objects of this class encompass options passed to GnuPG.
469 This class is responsible for determining command-line arguments576 This class is responsible for determining command-line arguments
470 which are based on options. It can be said that a GnuPG577 which are based on options. It can be said that a GnuPG
@@ -493,6 +600,8 @@
493600
494 * homedir601 * homedir
495 * default_key602 * default_key
603 * keyring
604 * secret_keyring
496 * comment605 * comment
497 * compress_algo606 * compress_algo
498 * options607 * options
@@ -536,38 +645,34 @@
536 ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']645 ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning']
537 """646 """
538647
648 booleans = ('armor', 'no_greeting', 'verbose', 'no_verbose',
649 'batch', 'always_trust', 'rfc1991', 'openpgp',
650 'quiet', 'no_options', 'textmode', 'force_v3_sigs')
651
652 metas = ('meta_pgp_5_compatible', 'meta_pgp_2_compatible',
653 'meta_interactive')
654
655 strings = ('homedir', 'default_key', 'comment', 'compress_algo',
656 'options', 'keyring', 'secret_keyring')
657
658 lists = ('encrypt_to', 'recipients')
659
660 __slots__ = booleans + metas + strings + lists + ('extra_args',)
661
539 def __init__(self):662 def __init__(self):
540 # booleans663 for b in self.booleans:
541 self.armor = 0664 setattr(self, b, 0)
542 self.no_greeting = 0
543 self.verbose = 0
544 self.no_verbose = 0
545 self.quiet = 0
546 self.batch = 0
547 self.always_trust = 0
548 self.rfc1991 = 0
549 self.openpgp = 0
550 self.force_v3_sigs = 0
551 self.no_options = 0
552 self.textmode = 0
553665
554 # meta-option booleans666 for m in self.metas:
555 self.meta_pgp_5_compatible = 0667 setattr(self, m, 0)
556 self.meta_pgp_2_compatible = 0
557 self.meta_interactive = 1668 self.meta_interactive = 1
558669
559 # strings670 for s in self.strings:
560 self.homedir = None671 setattr(self, s, None)
561 self.default_key = None672
562 self.comment = None673 for l in self.lists:
563 self.compress_algo = None674 setattr(self, l, [])
564 self.options = None675
565
566 # lists
567 self.encrypt_to = []
568 self.recipients = []
569
570 # miscellaneous arguments
571 self.extra_args = []676 self.extra_args = []
572677
573 def get_args( self ):678 def get_args( self ):
@@ -583,6 +688,8 @@
583 if self.comment != None: args.extend( [ '--comment', self.comment ] )688 if self.comment != None: args.extend( [ '--comment', self.comment ] )
584 if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] )689 if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] )
585 if self.default_key != None: args.extend( [ '--default-key', self.default_key ] )690 if self.default_key != None: args.extend( [ '--default-key', self.default_key ] )
691 if self.keyring != None: args.extend( [ '--keyring', self.keyring ] )
692 if self.secret_keyring != None: args.extend( [ '--secret-keyring', self.secret_keyring ] )
586693
587 if self.no_options: args.append( '--no-options' )694 if self.no_options: args.append( '--no-options' )
588 if self.armor: args.append( '--armor' )695 if self.armor: args.append( '--armor' )
@@ -615,7 +722,7 @@
615 return args722 return args
616723
617724
618class Process:725class Process(object):
619 """Objects of this class encompass properties of a GnuPG726 """Objects of this class encompass properties of a GnuPG
620 process spawned by GnuPG.run().727 process spawned by GnuPG.run().
621728
@@ -637,43 +744,24 @@
637 os.waitpid() to clean up the process, especially744 os.waitpid() to clean up the process, especially
638 if multiple calls are made to run().745 if multiple calls are made to run().
639 """746 """
747 __slots__ = ['_pipes', 'handles', 'pid', '_subproc']
640748
641 def __init__(self):749 def __init__(self):
642 self._pipes = {}750 self._pipes = {}
643 self.handles = {}751 self.handles = {}
644 self.pid = None752 self.pid = None
645 self._waited = None753 self._subproc = None
646 self.thread = None
647 self.returned = None
648754
649 def wait(self):755 def wait(self):
650 """756 """Wait on the process to exit, allowing for child cleanup.
651 Wait on threaded_waitpid to exit and examine results.757 Will raise an IOError if the process exits non-zero."""
652 Will raise an IOError if the process exits non-zero.758
653 """759 e = self._subproc.wait()
654 if self.returned == None:760 if e != 0:
655 self.thread.join()761 raise IOError("GnuPG exited non-zero, with code %d" % e)
656 if self.returned != 0:
657 raise IOError, "GnuPG exited non-zero, with code %d" % (self.returned >> 8)
658
659
660def threaded_waitpid(process):
661 """
662 When started as a thread with the Process object, thread
663 will execute an immediate waitpid() against the process
664 pid and will collect the process termination info. This
665 will allow us to reap child processes as soon as possible,
666 thus freeing resources quickly.
667 """
668 try:
669 process.returned = os.waitpid(process.pid, 0)[1]
670 except:
671 log.Debug("GPG process %d terminated before wait()" % process.pid)
672 process.returned = 0
673
674762
675def _run_doctests():763def _run_doctests():
676 import doctest, GnuPGInterface #@UnresolvedImport764 import doctest, GnuPGInterface
677 return doctest.testmod(GnuPGInterface)765 return doctest.testmod(GnuPGInterface)
678766
679# deprecated767# deprecated
680768
=== modified file 'duplicity/backend.py'
--- duplicity/backend.py 2010-08-09 18:56:03 +0000
+++ duplicity/backend.py 2010-10-25 15:49:45 +0000
@@ -64,7 +64,8 @@
64 @return: void64 @return: void
65 """65 """
66 path = duplicity.backends.__path__[0]66 path = duplicity.backends.__path__[0]
67 assert path.endswith("duplicity/backends"), duplicity.backends.__path__67 assert os.path.normcase(path).endswith("duplicity" + os.path.sep + "backends"), \
68 duplicity.backends.__path__
6869
69 files = os.listdir(path)70 files = os.listdir(path)
70 for fn in files:71 for fn in files:
@@ -201,6 +202,12 @@
201202
202 Raise InvalidBackendURL on invalid URL's203 Raise InvalidBackendURL on invalid URL's
203 """204 """
205
206 if sys.platform == "win32":
207 # Regex to match a path containing a Windows drive specifier
208 # Valid paths include "C:" "C:/" "C:stuff" "C:/stuff", not "C://stuff"
209 _drivespecre = re.compile("^[a-z]:(?![/\\\\]{2})", re.IGNORECASE)
210
204 def __init__(self, url_string):211 def __init__(self, url_string):
205 self.url_string = url_string212 self.url_string = url_string
206 _ensure_urlparser_initialized()213 _ensure_urlparser_initialized()
@@ -266,6 +273,12 @@
266 if not pu.scheme:273 if not pu.scheme:
267 return274 return
268275
276 # This happens with implicit local paths with a drive specifier
277 if sys.platform == "win32" and \
278 re.match(ParsedUrl._drivespecre, url_string):
279 self.scheme = ""
280 return
281
269 # Our backends do not handle implicit hosts.282 # Our backends do not handle implicit hosts.
270 if pu.scheme in urlparser.uses_netloc and not pu.hostname:283 if pu.scheme in urlparser.uses_netloc and not pu.hostname:
271 raise InvalidBackendURL("Missing hostname in a backend URL which "284 raise InvalidBackendURL("Missing hostname in a backend URL which "
272285
=== modified file 'duplicity/backends/localbackend.py'
--- duplicity/backends/localbackend.py 2010-07-22 19:15:11 +0000
+++ duplicity/backends/localbackend.py 2010-10-25 15:49:45 +0000
@@ -39,7 +39,16 @@
39 # The URL form "file:MyFile" is not a valid duplicity target.39 # The URL form "file:MyFile" is not a valid duplicity target.
40 if not parsed_url.path.startswith( '//' ):40 if not parsed_url.path.startswith( '//' ):
41 raise BackendException( "Bad file:// path syntax." )41 raise BackendException( "Bad file:// path syntax." )
42 self.remote_pathdir = path.Path(parsed_url.path[2:])42
43 # According to RFC 1738, file URLs take the form
44 # file://<hostname>/<path> where <hostname> == "" is localhost
45 # However, for backwards compatibility, interpret file://stuff/... as
46 # being a relative path starting with directory stuff
47 if parsed_url.path[2] == '/':
48 pathstr = path.from_url_path(parsed_url.path[3:], is_abs=True)
49 else:
50 pathstr = path.from_url_path(parsed_url.path[2:], is_abs=False)
51 self.remote_pathdir = path.Path(pathstr)
4352
44 def put(self, source_path, remote_filename = None, rename = None):53 def put(self, source_path, remote_filename = None, rename = None):
45 """If rename is set, try that first, copying if doesn't work"""54 """If rename is set, try that first, copying if doesn't work"""
4655
=== modified file 'duplicity/commandline.py'
--- duplicity/commandline.py 2010-10-06 15:57:51 +0000
+++ duplicity/commandline.py 2010-10-25 15:49:45 +0000
@@ -184,8 +184,21 @@
184 def set_log_fd(fd):184 def set_log_fd(fd):
185 if fd < 1:185 if fd < 1:
186 raise optparse.OptionValueError("log-fd must be greater than zero.")186 raise optparse.OptionValueError("log-fd must be greater than zero.")
187 if sys.platform == "win32":
188 # Convert OS file handle to C file descriptor
189 import msvcrt
190 fd = msvcrt.open_osfhandle(fd, 1) # 1 = _O_WRONLY
191 raise optparse.OptionValueError("Unable to open log-fd.")
187 log.add_fd(fd)192 log.add_fd(fd)
188193
194 def set_restore_dir(dir):
195 # Remove empty tail component, if any
196 head, tail = os.path.split(dir)
197 if not tail:
198 dir = head
199
200 globals.restore_dir = dir
201
189 def set_time_sep(sep, opt):202 def set_time_sep(sep, opt):
190 if sep == '-':203 if sep == '-':
191 raise optparse.OptionValueError("Dash ('-') not valid for time-separator.")204 raise optparse.OptionValueError("Dash ('-') not valid for time-separator.")
@@ -291,7 +304,7 @@
291 # --archive-dir <path>304 # --archive-dir <path>
292 parser.add_option("--file-to-restore", "-r", action="callback", type="file",305 parser.add_option("--file-to-restore", "-r", action="callback", type="file",
293 metavar=_("path"), dest="restore_dir",306 metavar=_("path"), dest="restore_dir",
294 callback=lambda o, s, v, p: setattr(p.values, "restore_dir", v.rstrip('/')))307 callback=set_restore_dir)
295308
296 # Used to confirm certain destructive operations like deleting old files.309 # Used to confirm certain destructive operations like deleting old files.
297 parser.add_option("--force", action="store_true")310 parser.add_option("--force", action="store_true")
298311
=== modified file 'duplicity/compilec.py'
--- duplicity/compilec.py 2009-04-01 15:07:45 +0000
+++ duplicity/compilec.py 2010-10-25 15:49:45 +0000
@@ -20,7 +20,9 @@
20# along with duplicity; if not, write to the Free Software Foundation,20# along with duplicity; if not, write to the Free Software Foundation,
21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA21# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2222
23import sys, os23import os
24import shutil
25import sys
24from distutils.core import setup, Extension26from distutils.core import setup, Extension
2527
26assert len(sys.argv) == 128assert len(sys.argv) == 1
@@ -33,5 +35,16 @@
33 ["_librsyncmodule.c"],35 ["_librsyncmodule.c"],
34 libraries=["rsync"])])36 libraries=["rsync"])])
3537
36assert not os.system("mv `find build -name _librsync.so` .")38def find_any_of(filenames, basedir="."):
37assert not os.system("rm -rf build")39 for dirpath, dirnames, dirfilenames in os.walk(basedir):
40 for filename in filenames:
41 if filename in dirfilenames:
42 return os.path.join(dirpath, filename)
43
44extfile = find_any_of(("_librsync.pyd", "_librsync.so"), "build")
45if not extfile:
46 sys.stderr.write("Can't find _librsync extension binary, build failed?\n")
47 sys.exit(1)
48
49os.rename(extfile, os.path.basename(extfile))
50shutil.rmtree("build")
3851
=== modified file 'duplicity/dup_temp.py'
--- duplicity/dup_temp.py 2010-07-22 19:15:11 +0000
+++ duplicity/dup_temp.py 2010-10-25 15:49:45 +0000
@@ -161,9 +161,11 @@
161 We have achieved the first checkpoint, make file visible and permanent.161 We have achieved the first checkpoint, make file visible and permanent.
162 """162 """
163 assert not globals.restart163 assert not globals.restart
164 self.tdp.rename(self.dirpath.append(self.partname))164 # Can't rename files open for write on Windows. Wait for close hook
165 if sys.platform != "win32":
166 self.tdp.rename(self.dirpath.append(self.partname))
167 del self.hooklist[0]
165 self.fileobj.flush()168 self.fileobj.flush()
166 del self.hooklist[0]
167169
168 def to_remote(self):170 def to_remote(self):
169 """171 """
@@ -173,13 +175,15 @@
173 pr = file_naming.parse(self.remname)175 pr = file_naming.parse(self.remname)
174 src = self.dirpath.append(self.partname)176 src = self.dirpath.append(self.partname)
175 tgt = self.dirpath.append(self.remname)177 tgt = self.dirpath.append(self.remname)
176 src_iter = SrcIter(src)
177 if pr.compressed:178 if pr.compressed:
179 src_iter = SrcIter(src)
178 gpg.GzipWriteFile(src_iter, tgt.name, size = sys.maxint)180 gpg.GzipWriteFile(src_iter, tgt.name, size = sys.maxint)
179 elif pr.encrypted:181 elif pr.encrypted:
182 src_iter = SrcIter(src)
180 gpg.GPGWriteFile(src_iter, tgt.name, globals.gpg_profile, size = sys.maxint)183 gpg.GPGWriteFile(src_iter, tgt.name, globals.gpg_profile, size = sys.maxint)
181 else:184 else:
182 os.system("cp -p %s %s" % (src.name, tgt.name))185 src.copy(tgt)
186 src.copy_attribs(tgt)
183 globals.backend.put(tgt) #@UndefinedVariable187 globals.backend.put(tgt) #@UndefinedVariable
184 os.unlink(tgt.name)188 os.unlink(tgt.name)
185189
@@ -189,9 +193,9 @@
189 """193 """
190 src = self.dirpath.append(self.partname)194 src = self.dirpath.append(self.partname)
191 tgt = self.dirpath.append(self.permname)195 tgt = self.dirpath.append(self.permname)
192 src_iter = SrcIter(src)
193 pr = file_naming.parse(self.permname)196 pr = file_naming.parse(self.permname)
194 if pr.compressed:197 if pr.compressed:
198 src_iter = SrcIter(src)
195 gpg.GzipWriteFile(src_iter, tgt.name, size = sys.maxint)199 gpg.GzipWriteFile(src_iter, tgt.name, size = sys.maxint)
196 os.unlink(src.name)200 os.unlink(src.name)
197 else:201 else:
198202
=== modified file 'duplicity/globals.py'
--- duplicity/globals.py 2010-08-26 13:01:10 +0000
+++ duplicity/globals.py 2010-10-25 15:49:45 +0000
@@ -21,7 +21,7 @@
2121
22"""Store global configuration information"""22"""Store global configuration information"""
2323
24import socket, os24import sys, socket, os
2525
26# The current version of duplicity26# The current version of duplicity
27version = "$version"27version = "$version"
@@ -36,16 +36,59 @@
36# The symbolic name of the backup being operated upon.36# The symbolic name of the backup being operated upon.
37backup_name = None37backup_name = None
3838
39# On Windows, use SHGetFolderPath for determining program directories
40if sys.platform == "win32":
41 import ctypes
42 import ctypes.wintypes as wintypes
43 windll = ctypes.windll
44
45 CSIDL_APPDATA = 0x001a
46 CSIDL_LOCAL_APPDATA = 0x001c
47 def get_csidl_folder_path(csidl):
48 SHGetFolderPath = windll.shell32.SHGetFolderPathW
49 SHGetFolderPath.argtypes = [
50 wintypes.HWND,
51 ctypes.c_int,
52 wintypes.HANDLE,
53 wintypes.DWORD,
54 wintypes.LPWSTR,
55 ]
56 folderpath = wintypes.create_unicode_buffer(wintypes.MAX_PATH)
57 result = SHGetFolderPath(0, csidl, 0, 0, folderpath)
58 if result != 0:
59 raise WindowsError(result, "Unable to get folder path")
60 return folderpath.value
61
62
39# Set to the Path of the archive directory (the directory which63# Set to the Path of the archive directory (the directory which
40# contains the signatures and manifests of the relevent backup64# contains the signatures and manifests of the relevent backup
41# collection), and for checkpoint state between volumes.65# collection), and for checkpoint state between volumes.
42# NOTE: this gets expanded in duplicity.commandline66# NOTE: this gets expanded in duplicity.commandline
43os.environ["XDG_CACHE_HOME"] = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))67if sys.platform == "win32":
44archive_dir = os.path.expandvars("$XDG_CACHE_HOME/duplicity")68 try:
69 archive_dir = get_csidl_folder_path(CSIDL_LOCAL_APPDATA)
70 except WindowsError:
71 try:
72 archive_dir = get_csidl_folder_path(CSIDL_APPDATA)
73 except WindowsError:
74 archive_dir = os.getenv("LOCALAPPDATA") or \
75 os.getenv("APPDATA") or \
76 os.path.expanduser("~")
77 archive_dir = os.path.join(archive_dir, "Duplicity", "Archives")
78else:
79 os.environ["XDG_CACHE_HOME"] = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
80 archive_dir = os.path.expandvars("$XDG_CACHE_HOME/duplicity")
4581
46# config dir for future use82# config dir for future use
47os.environ["XDG_CONFIG_HOME"] = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))83if sys.platform == "win32":
48config_dir = os.path.expandvars("$XDG_CONFIG_HOME/duplicity")84 try:
85 config_dir = get_csidl_folder_path(CSIDL_APPDATA)
86 except WindowsError:
87 config_dir = os.getenv("APPDATA") or os.path.expanduser("~")
88 config_dir = os.path.join(archive_dir, "Duplicity", "Config")
89else:
90 os.environ["XDG_CONFIG_HOME"] = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
91 config_dir = os.path.expandvars("$XDG_CONFIG_HOME/duplicity")
4992
50# Restores will try to bring back the state as of the following time.93# Restores will try to bring back the state as of the following time.
51# If it is None, default to current time.94# If it is None, default to current time.
5295
=== modified file 'duplicity/manifest.py'
--- duplicity/manifest.py 2010-07-22 19:15:11 +0000
+++ duplicity/manifest.py 2010-10-25 15:49:45 +0000
@@ -25,6 +25,7 @@
2525
26from duplicity import log26from duplicity import log
27from duplicity import globals27from duplicity import globals
28from duplicity import path
28from duplicity import util29from duplicity import util
2930
30class ManifestError(Exception):31class ManifestError(Exception):
@@ -67,7 +68,8 @@
67 if self.hostname:68 if self.hostname:
68 self.fh.write("Hostname %s\n" % self.hostname)69 self.fh.write("Hostname %s\n" % self.hostname)
69 if self.local_dirname:70 if self.local_dirname:
70 self.fh.write("Localdir %s\n" % Quote(self.local_dirname))71 self.fh.write("Localdir %s\n" % \
72 Quote(path.to_posix(self.local_dirname)))
71 return self73 return self
7274
73 def check_dirinfo(self):75 def check_dirinfo(self):
@@ -146,7 +148,8 @@
146 if self.hostname:148 if self.hostname:
147 result += "Hostname %s\n" % self.hostname149 result += "Hostname %s\n" % self.hostname
148 if self.local_dirname:150 if self.local_dirname:
149 result += "Localdir %s\n" % Quote(self.local_dirname)151 result += "Localdir %s\n" % \
152 Quote(path.to_posix(self.local_dirname))
150153
151 vol_num_list = self.volume_info_dict.keys()154 vol_num_list = self.volume_info_dict.keys()
152 vol_num_list.sort()155 vol_num_list.sort()
@@ -173,6 +176,9 @@
173 return Unquote(m.group(2))176 return Unquote(m.group(2))
174 self.hostname = get_field("hostname")177 self.hostname = get_field("hostname")
175 self.local_dirname = get_field("localdir")178 self.local_dirname = get_field("localdir")
179 if self.local_dirname:
180 self.local_dirname = path.from_posix(self.local_dirname,
181 globals.local_path and globals.local_path.name)
176182
177 next_vi_string_regexp = re.compile("(^|\\n)(volume\\s.*?)"183 next_vi_string_regexp = re.compile("(^|\\n)(volume\\s.*?)"
178 "(\\nvolume\\s|$)", re.I | re.S)184 "(\\nvolume\\s|$)", re.I | re.S)
@@ -221,7 +227,7 @@
221 Write string version of manifest to given path227 Write string version of manifest to given path
222 """228 """
223 assert not path.exists()229 assert not path.exists()
224 fout = path.open("w")230 fout = path.open("wb")
225 fout.write(self.to_string())231 fout.write(self.to_string())
226 assert not fout.close()232 assert not fout.close()
227 path.setdata()233 path.setdata()
228234
=== modified file 'duplicity/patchdir.py'
--- duplicity/patchdir.py 2010-07-22 19:15:11 +0000
+++ duplicity/patchdir.py 2010-10-25 15:49:45 +0000
@@ -19,7 +19,9 @@
19# along with duplicity; if not, write to the Free Software Foundation,19# along with duplicity; if not, write to the Free Software Foundation,
20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2121
22import os
22import re #@UnusedImport23import re #@UnusedImport
24import sys
23import types25import types
24import tempfile26import tempfile
2527
@@ -470,6 +472,19 @@
470 if not isinstance( current_file, file ):472 if not isinstance( current_file, file ):
471 # librsync needs true file473 # librsync needs true file
472 tempfp = tempfile.TemporaryFile( dir=globals.temproot )474 tempfp = tempfile.TemporaryFile( dir=globals.temproot )
475 if os.name == "nt":
476 # Temp wrapper is unnecessary, file opened O_TEMPORARY
477 tempfp = tempfp.file
478 elif os.name != "posix" and sys.platform != "cygwin":
479 # Note to future developers:
480 # librsync needs direct access to the underlying file.
481 # On these systems temporary files are wrapper objects
482 # The wrapper must be retained until file access is finished
483 # so that when it is released the file can be deleted
484 raise NotImplementedError(
485 "No support for direct access of temporary files " +
486 "on this platform")
487
473 misc.copyfileobj( current_file, tempfp )488 misc.copyfileobj( current_file, tempfp )
474 assert not current_file.close()489 assert not current_file.close()
475 tempfp.seek( 0 )490 tempfp.seek( 0 )
476491
=== modified file 'duplicity/path.py'
--- duplicity/path.py 2010-07-22 19:15:11 +0000
+++ duplicity/path.py 2010-10-25 15:49:45 +0000
@@ -41,6 +41,99 @@
41_copy_blocksize = 64 * 102441_copy_blocksize = 64 * 1024
42_tmp_path_counter = 142_tmp_path_counter = 1
4343
44def split_all(path):
45 """
46 Split path into components
47
48 Invariant: os.path.join(*split_all(path)) == path for a normalized path
49
50 @rtype list
51 @return List of components of path, beginning with "/" or a drive specifier
52 if absolute, ending with "" if path ended with a separator
53 """
54 parts = []
55 path, part = os.path.split(path)
56
57 # Special case for paths which end with a separator so rest is still split
58 if part == "":
59 parts.append(part)
60 path, part = os.path.split(path)
61
62 while path != "" and part != "":
63 parts.append(part)
64 path, part = os.path.split(path)
65
66 # Append root (path) for absolute path, or first relative component (part)
67 if path != "":
68 parts.append(path)
69 else:
70 parts.append(part)
71
72 parts.reverse()
73 return parts
74
75
76def from_posix(path, refpath=None):
77 """
78 Convert a POSIX-style path to the native path representation
79
80 Copy drive specification (if any) from refpath (if given)
81 """
82
83 # If native path representation is POSIX, no work needs to be done
84 if os.path.__name__ == "posixpath":
85 return path
86
87 parts = path.split("/")
88 if parts[0] == "":
89 parts[0] = os.path.sep
90
91 if refpath is not None:
92 drive = os.path.splitdrive(refpath)[0]
93 if drive:
94 parts.insert(0, drive)
95
96 return os.path.join(*parts)
97
98
99def to_posix(path):
100 """
101 Convert a path from the native path representation to a POSIX-style path
102
103 The path is broken into components according to split_all, then recombined
104 with "/" separating components. Any drive specifier is omitted.
105 """
106
107 # If native path representation is POSIX, no work needs to be done
108 if os.path.__name__ == "posixpath":
109 return path
110
111 parts = split_all(path)
112 if os.path.isabs(path):
113 return "/" + "/".join(parts[1:])
114 else:
115 return "/".join(parts)
116
117
118def from_url_path(url_path, is_abs=True):
119 """
120 Convert the <path> component of a file URL into a path in the native path
121 representation.
122 """
123
124 parts = url_path.split("/")
125 if is_abs:
126 if os.path.__name__ == "posixpath":
127 parts.insert(0, "/")
128 elif os.path.__name__ == "ntpath":
129 parts[0] += os.path.sep
130 else:
131 raise NotImplementedException(
132 "Method to create an absolute path not known")
133
134 return os.path.join(*parts)
135
136
44class StatResult:137class StatResult:
45 """Used to emulate the output of os.stat() and related"""138 """Used to emulate the output of os.stat() and related"""
46 # st_mode is required by the TarInfo class, but it's unclear how139 # st_mode is required by the TarInfo class, but it's unclear how
@@ -142,7 +235,7 @@
142 def get_relative_path(self):235 def get_relative_path(self):
143 """Return relative path, created from index"""236 """Return relative path, created from index"""
144 if self.index:237 if self.index:
145 return "/".join(self.index)238 return os.path.join(*self.index)
146 else:239 else:
147 return "."240 return "."
148241
@@ -435,7 +528,8 @@
435 def copy_attribs(self, other):528 def copy_attribs(self, other):
436 """Only copy attributes from self to other"""529 """Only copy attributes from self to other"""
437 if isinstance(other, Path):530 if isinstance(other, Path):
438 util.maybe_ignore_errors(lambda: os.chown(other.name, self.stat.st_uid, self.stat.st_gid))531 if hasattr(os, "chown"):
532 util.maybe_ignore_errors(lambda: os.chown(other.name, self.stat.st_uid, self.stat.st_gid))
439 util.maybe_ignore_errors(lambda: os.chmod(other.name, self.mode))533 util.maybe_ignore_errors(lambda: os.chmod(other.name, self.mode))
440 util.maybe_ignore_errors(lambda: os.utime(other.name, (time.time(), self.stat.st_mtime)))534 util.maybe_ignore_errors(lambda: os.utime(other.name, (time.time(), self.stat.st_mtime)))
441 other.setdata()535 other.setdata()
@@ -490,7 +584,7 @@
490 try:584 try:
491 self.stat = os.lstat(self.name)585 self.stat = os.lstat(self.name)
492 except OSError, e:586 except OSError, e:
493 err_string = errno.errorcode[e[0]]587 err_string = errno.errorcode.get(e[0])
494 if err_string == "ENOENT" or err_string == "ENOTDIR" or err_string == "ELOOP":588 if err_string == "ENOENT" or err_string == "ENOTDIR" or err_string == "ELOOP":
495 self.stat, self.type = None, None # file doesn't exist589 self.stat, self.type = None, None # file doesn't exist
496 self.mode = None590 self.mode = None
@@ -578,11 +672,7 @@
578 if self.index:672 if self.index:
579 return Path(self.base, self.index[:-1])673 return Path(self.base, self.index[:-1])
580 else:674 else:
581 components = self.base.split("/")675 return Path(os.path.dirname(self.base))
582 if len(components) == 2 and not components[0]:
583 return Path("/") # already in root directory
584 else:
585 return Path("/".join(components[:-1]))
586676
587 def writefileobj(self, fin):677 def writefileobj(self, fin):
588 """Copy file object fin to self. Close both when done."""678 """Copy file object fin to self. Close both when done."""
@@ -672,9 +762,7 @@
672762
673 def get_filename(self):763 def get_filename(self):
674 """Return filename of last component"""764 """Return filename of last component"""
675 components = self.name.split("/")765 return os.path.basename(self.name)
676 assert components and components[-1]
677 return components[-1]
678766
679 def get_canonical(self):767 def get_canonical(self):
680 """768 """
@@ -684,12 +772,9 @@
684 it's harder to remove "..", as "foo/bar/.." is not necessarily772 it's harder to remove "..", as "foo/bar/.." is not necessarily
685 "foo", so we can't use path.normpath()773 "foo", so we can't use path.normpath()
686 """774 """
687 newpath = "/".join(filter(lambda x: x and x != ".",775 pathparts = filter(lambda x: x and x != ".", split_all(self.name))
688 self.name.split("/")))776 if pathparts:
689 if self.name[0] == "/":777 return os.path.join(*pathparts)
690 return "/" + newpath
691 elif newpath:
692 return newpath
693 else:778 else:
694 return "."779 return "."
695780
696781
=== modified file 'duplicity/selection.py'
--- duplicity/selection.py 2010-07-22 19:15:11 +0000
+++ duplicity/selection.py 2010-10-25 15:49:45 +0000
@@ -23,12 +23,15 @@
23import re #@UnusedImport23import re #@UnusedImport
24import stat #@UnusedImport24import stat #@UnusedImport
2525
26from duplicity.path import * #@UnusedWildImport26from duplicity import path
27from duplicity import log #@Reimport27from duplicity import log #@Reimport
28from duplicity import globals #@Reimport28from duplicity import globals #@Reimport
29from duplicity import diffdir29from duplicity import diffdir
30from duplicity import util #@Reimport30from duplicity import util #@Reimport
3131
32# For convenience
33Path = path.Path
34
32"""Iterate exactly the requested files in a directory35"""Iterate exactly the requested files in a directory
3336
34Parses includes and excludes to yield correct files. More37Parses includes and excludes to yield correct files. More
@@ -93,6 +96,11 @@
93 self.rootpath = path96 self.rootpath = path
94 self.prefix = self.rootpath.name97 self.prefix = self.rootpath.name
9598
99 # Make sure prefix names a directory so prefix matching doesn't
100 # match partial directory names
101 if os.path.basename(self.prefix) != "":
102 self.prefix = os.path.join(self.prefix, "")
103
96 def set_iter(self):104 def set_iter(self):
97 """Initialize generator, prepare to iterate."""105 """Initialize generator, prepare to iterate."""
98 self.rootpath.setdata() # this may have changed since Select init106 self.rootpath.setdata() # this may have changed since Select init
@@ -381,7 +389,7 @@
381 if not line.startswith(self.prefix):389 if not line.startswith(self.prefix):
382 raise FilePrefixError(line)390 raise FilePrefixError(line)
383 line = line[len(self.prefix):] # Discard prefix391 line = line[len(self.prefix):] # Discard prefix
384 index = tuple(filter(lambda x: x, line.split("/"))) # remove empties392 index = tuple(path.split_all(line)) # remove empties
385 return (index, include)393 return (index, include)
386394
387 def filelist_pair_match(self, path, pair):395 def filelist_pair_match(self, path, pair):
@@ -532,8 +540,7 @@
532 """540 """
533 if not filename.startswith(self.prefix):541 if not filename.startswith(self.prefix):
534 raise FilePrefixError(filename)542 raise FilePrefixError(filename)
535 index = tuple(filter(lambda x: x,543 index = tuple(path.split_all(filename[len(self.prefix):]))
536 filename[len(self.prefix):].split("/")))
537 return self.glob_get_tuple_sf(index, include)544 return self.glob_get_tuple_sf(index, include)
538545
539 def glob_get_tuple_sf(self, tuple, include):546 def glob_get_tuple_sf(self, tuple, include):
@@ -614,17 +621,14 @@
614621
615 def glob_get_prefix_res(self, glob_str):622 def glob_get_prefix_res(self, glob_str):
616 """Return list of regexps equivalent to prefixes of glob_str"""623 """Return list of regexps equivalent to prefixes of glob_str"""
617 glob_parts = glob_str.split("/")624 glob_parts = path.split_all(glob_str)
618 if "" in glob_parts[1:-1]:625 if "" in glob_parts[1:-1]:
619 # "" OK if comes first or last, as in /foo/626 # "" OK if comes first or last, as in /foo/
620 raise GlobbingError("Consecutive '/'s found in globbing string "627 raise GlobbingError("Consecutive '/'s found in globbing string "
621 + glob_str)628 + glob_str)
622629
623 prefixes = map(lambda i: "/".join(glob_parts[:i+1]),630 prefixes = map(lambda i: os.path.join(*glob_parts[:i+1]),
624 range(len(glob_parts)))631 range(len(glob_parts)))
625 # we must make exception for root "/", only dir to end in slash
626 if prefixes[0] == "":
627 prefixes[0] = "/"
628 return map(self.glob_to_re, prefixes)632 return map(self.glob_to_re, prefixes)
629633
630 def glob_to_re(self, pat):634 def glob_to_re(self, pat):
@@ -638,6 +642,12 @@
638 by Donovan Baarda.642 by Donovan Baarda.
639643
640 """644 """
645 # Build regex for non-directory separator characters
646 notsep = os.path.sep
647 if os.path.altsep:
648 notsep += os.path.altsep
649 notsep = "[^" + notsep.replace("\\", "\\\\") + "]"
650
641 i, n, res = 0, len(pat), ''651 i, n, res = 0, len(pat), ''
642 while i < n:652 while i < n:
643 c, s = pat[i], pat[i:i+2]653 c, s = pat[i], pat[i:i+2]
@@ -646,9 +656,9 @@
646 res = res + '.*'656 res = res + '.*'
647 i = i + 1657 i = i + 1
648 elif c == '*':658 elif c == '*':
649 res = res + '[^/]*'659 res = res + notsep + '*'
650 elif c == '?':660 elif c == '?':
651 res = res + '[^/]'661 res = res + notsep
652 elif c == '[':662 elif c == '[':
653 j = i663 j = i
654 if j < n and pat[j] in '!^':664 if j < n and pat[j] in '!^':
655665
=== modified file 'duplicity/tarfile.py'
--- duplicity/tarfile.py 2010-07-22 19:15:11 +0000
+++ duplicity/tarfile.py 2010-10-25 15:49:45 +0000
@@ -1683,8 +1683,10 @@
1683def set_pwd_dict():1683def set_pwd_dict():
1684 """Set global pwd caching dictionaries uid_dict and uname_dict"""1684 """Set global pwd caching dictionaries uid_dict and uname_dict"""
1685 global uid_dict, uname_dict1685 global uid_dict, uname_dict
1686 assert uid_dict is None and uname_dict is None and pwd1686 assert uid_dict is None and uname_dict is None
1687 uid_dict = {}; uname_dict = {}1687 uid_dict = {}; uname_dict = {}
1688 if pwd is None:
1689 return
1688 for entry in pwd.getpwall():1690 for entry in pwd.getpwall():
1689 uname = entry[0]; uid = entry[2]1691 uname = entry[0]; uid = entry[2]
1690 uid_dict[uid] = uname1692 uid_dict[uid] = uname
@@ -1702,8 +1704,10 @@
17021704
1703def set_grp_dict():1705def set_grp_dict():
1704 global gid_dict, gname_dict1706 global gid_dict, gname_dict
1705 assert gid_dict is None and gname_dict is None and grp1707 assert gid_dict is None and gname_dict is None
1706 gid_dict = {}; gname_dict = {}1708 gid_dict = {}; gname_dict = {}
1709 if grp is None:
1710 return
1707 for entry in grp.getgrall():1711 for entry in grp.getgrall():
1708 gname = entry[0]; gid = entry[2]1712 gname = entry[0]; gid = entry[2]
1709 gid_dict[gid] = gname1713 gid_dict[gid] = gname
17101714
=== removed file 'po/update-pot'
--- po/update-pot 2009-09-15 02:13:01 +0000
+++ po/update-pot 1970-01-01 00:00:00 +0000
@@ -1,4 +0,0 @@
1#!/bin/sh
2
3intltool-update --pot -g duplicity
4sed -e 's/^#\. TRANSL:/#./' -i duplicity.pot
50
=== added file 'po/update-pot.py'
--- po/update-pot.py 1970-01-01 00:00:00 +0000
+++ po/update-pot.py 2010-10-25 15:49:45 +0000
@@ -0,0 +1,21 @@
1#!/usr/bin/env python
2
3import os
4import re
5import sys
6import tempfile
7
8retval = os.system('intltool-update --pot -g duplicity')
9if retval != 0:
10 # intltool-update failed and already wrote errors, propagate failure
11 sys.exit(retval)
12
13replre = re.compile('^#\. TRANSL:')
14with open("duplicity.pot", "rb") as potfile:
15 with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
16 tmpfilename = tmpfile.name
17 for line in potfile:
18 tmpfile.write(re.sub(replre, "", line))
19
20os.remove("duplicity.pot")
21os.rename(tmpfilename, "duplicity.pot")

Subscribers

People subscribed via source and target branches

to all changes: