Merge lp:~mterry/duplicity/gio into lp:~duplicity-team/duplicity/trunk

Proposed by Kenneth Loafman
Status: Merged
Merged at revision: 527
Proposed branch: lp:~mterry/duplicity/gio
Merge into: lp:~duplicity-team/duplicity/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~mterry/duplicity/gio
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+7619@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote :

This code looks clean, let's add it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'duplicity.1'
2--- duplicity.1 2009-05-13 18:20:25 +0000
3+++ duplicity.1 2009-06-04 17:57:56 +0000
4@@ -335,6 +335,10 @@
5 Use regular (PORT) data connections.
6
7 .TP
8+.B --gio
9+Use the GIO backend and interpret any URLs as GIO would.
10+
11+.TP
12 .BI "--imap-mailbox " option
13 Allows you to specify a different mailbox. The default is
14 "INBOX".
15
16=== modified file 'duplicity/backend.py'
17--- duplicity/backend.py 2009-04-02 14:47:12 +0000
18+++ duplicity/backend.py 2009-06-04 17:57:56 +0000
19@@ -47,8 +47,14 @@
20 # todo: this should really NOT be done here
21 socket.setdefaulttimeout(globals.timeout)
22
23+_forced_backend = None
24 _backends = {}
25
26+def force_backend(backend):
27+ """Forces the use of a particular backend, regardless of schema"""
28+ global _forced_backend
29+ _forced_backend = backend
30+
31 def register_backend(scheme, backend_factory):
32 """
33 Register a given backend factory responsible for URL:s with the
34@@ -87,9 +93,11 @@
35 if not pu.scheme:
36 return None
37
38- global _backends
39+ global _backends, _forced_backend
40
41- if not pu.scheme in _backends:
42+ if _forced_backend:
43+ return _forced_backend(pu)
44+ elif not pu.scheme in _backends:
45 raise UnsupportedBackendScheme(url_string)
46 else:
47 return _backends[pu.scheme](pu)
48
49=== added file 'duplicity/backends/giobackend.py'
50--- duplicity/backends/giobackend.py 1970-01-01 00:00:00 +0000
51+++ duplicity/backends/giobackend.py 2009-06-04 18:07:07 +0000
52@@ -0,0 +1,144 @@
53+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
54+#
55+# Copyright 2009 Michael Terry <mike@mterry.name>
56+#
57+# This file is part of duplicity.
58+#
59+# Duplicity is free software; you can redistribute it and/or modify it
60+# under the terms of the GNU General Public License as published by the
61+# Free Software Foundation; either version 2 of the License, or (at your
62+# option) any later version.
63+#
64+# Duplicity is distributed in the hope that it will be useful, but
65+# WITHOUT ANY WARRANTY; without even the implied warranty of
66+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
67+# General Public License for more details.
68+#
69+# You should have received a copy of the GNU General Public License
70+# along with duplicity; if not, write to the Free Software Foundation,
71+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
72+
73+import os
74+import types
75+import subprocess
76+import atexit
77+import signal
78+import gio
79+import glib
80+
81+import duplicity.backend
82+from duplicity import log
83+from duplicity import globals
84+from duplicity.errors import *
85+from duplicity.util import exception_traceback
86+
87+def ensure_dbus():
88+ # GIO requires a dbus session bus which can start the gvfs daemons
89+ # when required. So we make sure that such a bus exists and that our
90+ # environment points to it.
91+ if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
92+ output = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE).communicate()[0]
93+ lines = output.split('\n')
94+ for line in lines:
95+ parts = line.split('=', 1)
96+ if len(parts) == 2:
97+ if parts[0] == 'DBUS_SESSION_BUS_PID': # cleanup at end
98+ atexit.register(os.kill, int(parts[1]), signal.SIGTERM)
99+ os.environ[parts[0]] = parts[1]
100+
101+class DupMountOperation(gio.MountOperation):
102+ """A simple MountOperation that grabs the password from the environment
103+ or the user.
104+ """
105+ def __init__(self, backend):
106+ gio.MountOperation.__init__(self)
107+ self.backend = backend
108+ self.connect('ask-password', self.ask_password)
109+
110+ def ask_password(self, *args, **kwargs):
111+ self.set_password(self.backend.get_password())
112+ self.reply(gio.MOUNT_OPERATION_HANDLED)
113+
114+class GIOBackend(duplicity.backend.Backend):
115+ """Use this backend when saving to a GIO URL.
116+ This is a bit of a meta-backend, in that it can handle multiple schemas.
117+ URLs look like schema://user@server/path.
118+ """
119+ def __init__(self, parsed_url):
120+ duplicity.backend.Backend.__init__(self, parsed_url)
121+
122+ ensure_dbus()
123+
124+ self.remote_file = gio.File(uri=parsed_url.url_string)
125+
126+ # Now we make sure the location is mounted
127+ op = DupMountOperation(self)
128+ loop = glib.MainLoop()
129+ self.remote_file.mount_enclosing_volume(op, self.done_with_mount,
130+ 0, user_data=loop)
131+ loop.run() # halt program until we're done mounting
132+
133+ def done_with_mount(self, fileobj, result, loop):
134+ try:
135+ fileobj.mount_enclosing_volume_finish(result)
136+ except gio.Error, e:
137+ # check for NOT_SUPPORTED because some schemas (e.g. file://) validly don't
138+ if e.code != gio.ERROR_ALREADY_MOUNTED and e.code != gio.ERROR_NOT_SUPPORTED:
139+ log.FatalError(_("Connection failed, please check your password: %s")
140+ % str(e), log.ErrorCode.connection_failed)
141+ loop.quit()
142+
143+ def copy_progress(self, *args, **kwargs):
144+ pass
145+
146+ def copy_file(self, source, target):
147+ for n in range(1, globals.num_retries+1):
148+ log.Info(_("Writing %s") % target.get_parse_name())
149+ try:
150+ source.copy(target, self.copy_progress,
151+ gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
152+ return
153+ except Exception, e:
154+ log.Warn("Write of '%s' failed (attempt %s): %s: %s"
155+ % (target.get_parse_name(), n, e.__class__.__name__, str(e)))
156+ log.Debug("Backtrace of previous error: %s"
157+ % exception_traceback())
158+ raise BackendException(_("Could not copy %s to %s") % (source.get_parse_name(),
159+ target.get_parse_name()))
160+
161+ def put(self, source_path, remote_filename = None):
162+ """Copy file to remote"""
163+ if not remote_filename:
164+ remote_filename = source_path.get_filename()
165+ source_file = gio.File(path=source_path.name)
166+ target_file = self.remote_file.get_child_for_display_name(remote_filename)
167+ self.copy_file(source_file, target_file)
168+
169+ def get(self, filename, local_path):
170+ """Get file and put in local_path (Path object)"""
171+ source_file = self.remote_file.get_child_for_display_name(filename)
172+ target_file = gio.File(path=local_path.name)
173+ self.copy_file(source_file, target_file)
174+
175+ def list(self):
176+ """List files in that directory"""
177+ enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
178+ gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
179+ files = []
180+ try:
181+ info = enum.next_file()
182+ while info:
183+ files.append(info.get_display_name())
184+ info = enum.next_file()
185+ return files
186+ except Exception, e:
187+ raise BackendException(str(e))
188+
189+ def delete(self, filename_list):
190+ """Delete all files in filename list"""
191+ assert type(filename_list) is not types.StringType
192+ try:
193+ for filename in filename_list:
194+ self.remote_file.get_child_for_display_name(filename).delete()
195+ except Exception, e:
196+ raise BackendException(str(e))
197
198=== modified file 'duplicity/commandline.py'
199--- duplicity/commandline.py 2009-05-13 18:20:25 +0000
200+++ duplicity/commandline.py 2009-06-04 17:57:56 +0000
201@@ -74,6 +74,7 @@
202 "ftp-passive",
203 "ftp-regular",
204 "full-if-older-than=",
205+ "gio",
206 "gpg-options=",
207 "help",
208 "imap-full-address",
209@@ -235,6 +236,9 @@
210 globals.ftp_connection = 'regular'
211 elif opt == "--imap-mailbox":
212 globals.imap_mailbox = arg.strip()
213+ elif opt == "--gio":
214+ import duplicity.backends.giobackend
215+ backend.force_backend(duplicity.backends.giobackend.GIOBackend)
216 elif opt == "--gpg-options":
217 gpg.gpg_options = (gpg.gpg_options + ' ' + arg).strip()
218 elif opt in ["-h", "--help"]:
219@@ -410,6 +414,7 @@
220 --force
221 --ftp-passive
222 --ftp-regular
223+ --gio
224 --gpg-options
225 --include <shell_pattern>
226 --include-filelist <filename>
227
228=== modified file 'testing/backendtest.py'
229--- testing/backendtest.py 2009-04-02 14:47:12 +0000
230+++ testing/backendtest.py 2009-06-04 17:57:56 +0000
231@@ -25,6 +25,7 @@
232
233 import duplicity.backend
234 import duplicity.backends
235+import duplicity.backends.giobackend
236 from duplicity.errors import *
237 from duplicity import path, log, file_naming, dup_time, globals, gpg
238
239@@ -195,5 +196,35 @@
240 password = config.webdavs_password
241
242
243+class GIOTest(UnivTest):
244+ """ Generic gio module backend class """
245+ def setUp(self):
246+ duplicity.backend.force_backend(duplicity.backends.giobackend.GIOBackend)
247+
248+ def tearDown(self):
249+ duplicity.backend.force_backend(None)
250+
251+
252+class gioFileModuleTest(GIOTest, unittest.TestCase):
253+ """ Test the gio file module backend """
254+ my_test_id = "gio/file"
255+ url_string = config.file_url
256+ password = config.file_password
257+
258+
259+class gioSSHModuleTest(GIOTest, unittest.TestCase):
260+ """ Test the gio ssh module backend """
261+ my_test_id = "gio/ssh"
262+ url_string = config.ssh_url
263+ password = config.ssh_password
264+
265+
266+class gioFTPModuleTest(GIOTest, unittest.TestCase):
267+ """ Test the gio ftp module backend """
268+ my_test_id = "gio/ftp"
269+ url_string = config.ftp_url
270+ password = config.ftp_password
271+
272+
273 if __name__ == "__main__":
274 unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: