1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
3
# Copyright 2009 Michael Terry <mike@mterry.name>
5
# This file is part of duplicity.
7
# Duplicity is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by the
9
# Free Software Foundation; either version 2 of the License, or (at your
10
# option) any later version.
12
# Duplicity is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# General Public License for more details.
17
# You should have received a copy of the GNU General Public License
18
# along with duplicity; if not, write to the Free Software Foundation,
19
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
29
import duplicity.backend
30
from duplicity import log
31
from duplicity import globals
32
from duplicity.errors import *
33
from duplicity.util import exception_traceback
36
# GIO requires a dbus session bus which can start the gvfs daemons
37
# when required. So we make sure that such a bus exists and that our
38
# environment points to it.
39
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
40
output = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE).communicate()[0]
41
lines = output.split('\n')
43
parts = line.split('=', 1)
45
if parts[0] == 'DBUS_SESSION_BUS_PID': # cleanup at end
46
atexit.register(os.kill, int(parts[1]), signal.SIGTERM)
47
os.environ[parts[0]] = parts[1]
49
class DupMountOperation(gio.MountOperation):
50
"""A simple MountOperation that grabs the password from the environment
53
def __init__(self, backend):
54
gio.MountOperation.__init__(self)
55
self.backend = backend
56
self.connect('ask-password', self.ask_password)
58
def ask_password(self, *args, **kwargs):
59
self.set_password(self.backend.get_password())
60
self.reply(gio.MOUNT_OPERATION_HANDLED)
62
class GIOBackend(duplicity.backend.Backend):
63
"""Use this backend when saving to a GIO URL.
64
This is a bit of a meta-backend, in that it can handle multiple schemas.
65
URLs look like schema://user@server/path.
67
def __init__(self, parsed_url):
68
duplicity.backend.Backend.__init__(self, parsed_url)
72
self.remote_file = gio.File(uri=parsed_url.url_string)
74
# Now we make sure the location is mounted
75
op = DupMountOperation(self)
76
loop = glib.MainLoop()
77
self.remote_file.mount_enclosing_volume(op, self.done_with_mount,
79
loop.run() # halt program until we're done mounting
81
def done_with_mount(self, fileobj, result, loop):
83
fileobj.mount_enclosing_volume_finish(result)
85
# check for NOT_SUPPORTED because some schemas (e.g. file://) validly don't
86
if e.code != gio.ERROR_ALREADY_MOUNTED and e.code != gio.ERROR_NOT_SUPPORTED:
87
log.FatalError(_("Connection failed, please check your password: %s")
88
% str(e), log.ErrorCode.connection_failed)
91
def copy_progress(self, *args, **kwargs):
94
def copy_file(self, source, target):
95
for n in range(1, globals.num_retries+1):
96
log.Info(_("Writing %s") % target.get_parse_name())
98
source.copy(target, self.copy_progress,
99
gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
102
log.Warn("Write of '%s' failed (attempt %s): %s: %s"
103
% (target.get_parse_name(), n, e.__class__.__name__, str(e)))
104
log.Debug("Backtrace of previous error: %s"
105
% exception_traceback())
106
raise BackendException(_("Could not copy %s to %s") % (source.get_parse_name(),
107
target.get_parse_name()))
109
def put(self, source_path, remote_filename = None):
110
"""Copy file to remote"""
111
if not remote_filename:
112
remote_filename = source_path.get_filename()
113
source_file = gio.File(path=source_path.name)
114
target_file = self.remote_file.get_child_for_display_name(remote_filename)
115
self.copy_file(source_file, target_file)
117
def get(self, filename, local_path):
118
"""Get file and put in local_path (Path object)"""
119
source_file = self.remote_file.get_child_for_display_name(filename)
120
target_file = gio.File(path=local_path.name)
121
self.copy_file(source_file, target_file)
125
"""List files in that directory"""
126
enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
127
gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
130
info = enum.next_file()
132
files.append(info.get_display_name())
133
info = enum.next_file()
136
raise BackendException(str(e))
138
def delete(self, filename_list):
139
"""Delete all files in filename list"""
140
assert type(filename_list) is not types.StringType
142
for filename in filename_list:
143
self.remote_file.get_child_for_display_name(filename).delete()
145
raise BackendException(str(e))