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
26
import gio #@UnresolvedImport
27
import glib #@UnresolvedImport
29
import duplicity.backend
30
from duplicity.backend import retry
31
from duplicity import log
32
from duplicity import globals
33
from duplicity import util
34
from duplicity.errors import * #@UnusedWildImport
35
from duplicity.util import exception_traceback
38
# GIO requires a dbus session bus which can start the gvfs daemons
39
# when required. So we make sure that such a bus exists and that our
40
# environment points to it.
41
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
42
output = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE).communicate()[0]
43
lines = output.split('\n')
45
parts = line.split('=', 1)
47
if parts[0] == 'DBUS_SESSION_BUS_PID': # cleanup at end
48
atexit.register(os.kill, int(parts[1]), signal.SIGTERM)
49
os.environ[parts[0]] = parts[1]
51
class DupMountOperation(gio.MountOperation):
52
"""A simple MountOperation that grabs the password from the environment
55
def __init__(self, backend):
56
gio.MountOperation.__init__(self)
57
self.backend = backend
58
self.connect('ask-password', self.ask_password)
60
def ask_password(self, *args, **kwargs):
61
self.set_password(self.backend.get_password())
62
self.reply(gio.MOUNT_OPERATION_HANDLED)
64
class GIOBackend(duplicity.backend.Backend):
65
"""Use this backend when saving to a GIO URL.
66
This is a bit of a meta-backend, in that it can handle multiple schemas.
67
URLs look like schema://user@server/path.
69
def __init__(self, parsed_url):
70
duplicity.backend.Backend.__init__(self, parsed_url)
74
self.remote_file = gio.File(uri=parsed_url.url_string)
76
# Now we make sure the location is mounted
77
op = DupMountOperation(self)
78
loop = glib.MainLoop()
79
self.remote_file.mount_enclosing_volume(op, self.done_with_mount,
81
loop.run() # halt program until we're done mounting
83
def done_with_mount(self, fileobj, result, loop):
85
fileobj.mount_enclosing_volume_finish(result)
87
# check for NOT_SUPPORTED because some schemas (e.g. file://) validly don't
88
if e.code != gio.ERROR_ALREADY_MOUNTED and e.code != gio.ERROR_NOT_SUPPORTED:
89
log.FatalError(_("Connection failed, please check your password: %s")
90
% str(e), log.ErrorCode.connection_failed)
93
def handle_error(self, raise_error, e, op, file1=None, file2=None):
96
code = log.ErrorCode.backend_error
97
if isinstance(e, gio.Error):
98
if e.code == gio.ERROR_PERMISSION_DENIED:
99
code = log.ErrorCode.backend_permission_denied
100
elif e.code == gio.ERROR_NOT_FOUND:
101
code = log.ErrorCode.backend_not_found
102
elif e.code == gio.ERROR_NO_SPACE:
103
code = log.ErrorCode.backend_no_space
104
extra = ' '.join([util.escape(x) for x in [file1, file2] if x])
105
extra = ' '.join([op, extra])
106
log.FatalError(str(e), code, extra)
108
def copy_progress(self, *args, **kwargs):
112
def copy_file(self, op, source, target, raise_errors=False):
113
log.Info(_("Writing %s") % target.get_parse_name())
115
source.copy(target, self.copy_progress,
116
gio.FILE_COPY_OVERWRITE | gio.FILE_COPY_NOFOLLOW_SYMLINKS)
118
self.handle_error(raise_errors, e, op, source.get_parse_name(),
119
target.get_parse_name())
121
def put(self, source_path, remote_filename = None):
122
"""Copy file to remote"""
123
if not remote_filename:
124
remote_filename = source_path.get_filename()
125
source_file = gio.File(path=source_path.name)
126
target_file = self.remote_file.get_child(remote_filename)
127
self.copy_file('put', source_file, target_file)
129
def get(self, filename, local_path):
130
"""Get file and put in local_path (Path object)"""
131
source_file = self.remote_file.get_child(filename)
132
target_file = gio.File(path=local_path.name)
133
self.copy_file('get', source_file, target_file)
137
def list(self, raise_errors=False):
138
"""List files in that directory"""
141
enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME,
142
gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
143
info = enum.next_file()
145
files.append(info.get_name())
146
info = enum.next_file()
148
self.handle_error(raise_errors, e, 'list',
149
self.remote_file.get_parse_name())
153
def delete(self, filename_list, raise_errors=False):
154
"""Delete all files in filename list"""
155
assert type(filename_list) is not types.StringType
156
for filename in filename_list:
157
target_file = self.remote_file.get_child(filename)
161
if isinstance(e, gio.Error):
162
if e.code == gio.ERROR_NOT_FOUND:
164
self.handle_error(raise_errors, e, 'delete',
165
target_file.get_parse_name())