1
# Copyright (C) 2010 Canonical Ltd.
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
# Author: Mattias Eriksson
19
"""Implementation of Transport over gio.
21
Written by Mattias Eriksson <snaggen@acc.umu.se> based on the ftp transport.
23
It provides the gio+XXX:// protocols where XXX is any of the protocols
26
from cStringIO import StringIO
46
from bzrlib.trace import mutter, warning
47
from bzrlib.transport import (
54
from bzrlib.tests.test_server import TestServer
58
except ImportError, e:
59
raise errors.DependencyNotPresent('glib', e)
62
except ImportError, e:
63
raise errors.DependencyNotPresent('gio', e)
66
class GioLocalURLServer(TestServer):
67
"""A pretend server for local transports, using file:// urls.
69
Of course no actual server is required to access the local filesystem, so
70
this just exists to tell the test code how to get to it.
73
def start_server(self):
77
"""See Transport.Server.get_url."""
78
return "gio+" + urlutils.local_path_to_url('')
81
class GioFileStream(FileStream):
82
"""A file stream object returned by open_write_stream.
84
This version uses GIO to perform writes.
87
def __init__(self, transport, relpath):
88
FileStream.__init__(self, transport, relpath)
89
self.gio_file = transport._get_GIO(relpath)
90
self.stream = self.gio_file.create()
95
def write(self, bytes):
97
#Using pump_string_file seems to make things crash
98
osutils.pumpfile(StringIO(bytes), self.stream)
100
#self.transport._translate_gio_error(e,self.relpath)
101
raise errors.BzrError(str(e))
104
class GioStatResult(object):
106
def __init__(self, f):
107
info = f.query_info('standard::size,standard::type')
108
self.st_size = info.get_size()
109
type = info.get_file_type()
110
if (type == gio.FILE_TYPE_REGULAR):
111
self.st_mode = stat.S_IFREG
112
elif type == gio.FILE_TYPE_DIRECTORY:
113
self.st_mode = stat.S_IFDIR
116
class GioTransport(ConnectedTransport):
117
"""This is the transport agent for gio+XXX:// access."""
119
def __init__(self, base, _from_transport=None):
120
"""Initialize the GIO transport and make sure the url is correct."""
122
if not base.startswith('gio+'):
123
raise ValueError(base)
125
(scheme, netloc, path, params, query, fragment) = \
126
urlparse.urlparse(base[len('gio+'):], allow_fragments=False)
128
user, netloc = netloc.rsplit('@', 1)
129
#Seems it is not possible to list supported backends for GIO
130
#so a hardcoded list it is then.
131
gio_backends = ['dav', 'file', 'ftp', 'obex', 'sftp', 'ssh', 'smb']
132
if scheme not in gio_backends:
133
raise errors.InvalidURL(base,
134
extra="GIO support is only available for " + \
135
', '.join(gio_backends))
137
#Remove the username and password from the url we send to GIO
138
#by rebuilding the url again.
139
u = (scheme, netloc, path, '', '', '')
140
self.url = urlparse.urlunparse(u)
142
# And finally initialize super
143
super(GioTransport, self).__init__(base,
144
_from_transport=_from_transport)
146
def _relpath_to_url(self, relpath):
147
full_url = urlutils.join(self.url, relpath)
148
if isinstance(full_url, unicode):
149
raise errors.InvalidURL(full_url)
152
def _get_GIO(self, relpath):
153
"""Return the ftplib.GIO instance for this object."""
154
# Ensures that a connection is established
155
connection = self._get_connection()
156
if connection is None:
157
# First connection ever
158
connection, credentials = self._create_connection()
159
self._set_connection(connection, credentials)
160
fileurl = self._relpath_to_url(relpath)
161
file = gio.File(fileurl)
164
def _auth_cb(self, op, message, default_user, default_domain, flags):
165
#really use bzrlib.auth get_password for this
166
#or possibly better gnome-keyring?
167
auth = config.AuthenticationConfig()
168
(scheme, urluser, urlpassword, host, port, urlpath) = \
169
urlutils.parse_url(self.url)
171
if (flags & gio.ASK_PASSWORD_NEED_USERNAME and
172
flags & gio.ASK_PASSWORD_NEED_DOMAIN):
173
prompt = scheme.upper() + ' %(host)s DOMAIN\username'
174
user_and_domain = auth.get_user(scheme, host,
175
port=port, ask=True, prompt=prompt)
176
(domain, user) = user_and_domain.split('\\', 1)
177
op.set_username(user)
178
op.set_domain(domain)
179
elif flags & gio.ASK_PASSWORD_NEED_USERNAME:
180
user = auth.get_user(scheme, host,
182
op.set_username(user)
183
elif flags & gio.ASK_PASSWORD_NEED_DOMAIN:
184
#Don't know how common this case is, but anyway
185
#a DOMAIN and a username prompt should be the
186
#same so I will missuse the ui_factory get_username
188
prompt = scheme.upper() + ' %(host)s DOMAIN'
189
domain = ui.ui_factory.get_username(prompt=prompt)
190
op.set_domain(domain)
192
if flags & gio.ASK_PASSWORD_NEED_PASSWORD:
194
user = op.get_username()
195
password = auth.get_password(scheme, host,
197
op.set_password(password)
198
op.reply(gio.MOUNT_OPERATION_HANDLED)
200
def _mount_done_cb(self, obj, res):
202
obj.mount_enclosing_volume_finish(res)
206
raise errors.BzrError("Failed to mount the given location: " + str(e));
208
def _create_connection(self, credentials=None):
209
if credentials is None:
210
user, password = self._user, self._password
212
user, password = credentials
215
connection = gio.File(self.url)
218
mount = connection.find_enclosing_mount()
220
if (e.code == gio.ERROR_NOT_MOUNTED):
221
self.loop = glib.MainLoop()
222
ui.ui_factory.show_message('Mounting %s using GIO' % \
224
op = gio.MountOperation()
226
op.set_username(user)
228
op.set_password(password)
229
op.connect('ask-password', self._auth_cb)
230
m = connection.mount_enclosing_volume(op,
234
raise errors.TransportError(msg="Error setting up connection:"
235
" %s" % str(e), orig_error=e)
236
return connection, (user, password)
238
def _reconnect(self):
239
"""Create a new connection with the previously used credentials"""
240
credentials = self._get_credentials()
241
connection, credentials = self._create_connection(credentials)
242
self._set_connection(connection, credentials)
244
def _remote_path(self, relpath):
245
relative = urlutils.unescape(relpath).encode('utf-8')
246
remote_path = self._combine_paths(self._path, relative)
249
def has(self, relpath):
250
"""Does the target location exist?"""
252
if 'gio' in debug.debug_flags:
253
mutter('GIO has check: %s' % relpath)
254
f = self._get_GIO(relpath)
255
st = GioStatResult(f)
256
if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
260
if e.code == gio.ERROR_NOT_FOUND:
263
self._translate_gio_error(e, relpath)
265
def get(self, relpath, decode=False, retries=0):
266
"""Get the file at the given relative path.
268
:param relpath: The relative path to the file
269
:param retries: Number of retries after temporary failures so far
272
We're meant to return a file-like object which bzr will
273
then read from. For now we do this via the magic of StringIO
276
if 'gio' in debug.debug_flags:
277
mutter("GIO get: %s" % relpath)
278
f = self._get_GIO(relpath)
285
#If we get a not mounted here it might mean
286
#that a bad path has been entered (or that mount failed)
287
if (e.code == gio.ERROR_NOT_MOUNTED):
288
raise errors.PathError(relpath,
289
extra='Failed to get file, make sure the path is correct. ' \
292
self._translate_gio_error(e, relpath)
294
def put_file(self, relpath, fp, mode=None):
295
"""Copy the file-like object into the location.
297
:param relpath: Location to put the contents, relative to base.
298
:param fp: File-like or string object.
300
if 'gio' in debug.debug_flags:
301
mutter("GIO put_file %s" % relpath)
302
tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
303
os.getpid(), random.randint(0, 0x7FFFFFFF))
309
f = self._get_GIO(tmppath)
312
length = self._pump(fp, fout)
316
dest = self._get_GIO(relpath)
317
f.move(dest, flags=gio.FILE_COPY_OVERWRITE)
320
self._setmode(relpath, mode)
323
self._translate_gio_error(e, relpath)
325
if not closed and fout is not None:
327
if f is not None and f.query_exists():
330
def mkdir(self, relpath, mode=None):
331
"""Create a directory at the given path."""
333
if 'gio' in debug.debug_flags:
334
mutter("GIO mkdir: %s" % relpath)
335
f = self._get_GIO(relpath)
337
self._setmode(relpath, mode)
339
self._translate_gio_error(e, relpath)
341
def open_write_stream(self, relpath, mode=None):
342
"""See Transport.open_write_stream."""
343
if 'gio' in debug.debug_flags:
344
mutter("GIO open_write_stream %s" % relpath)
346
self._setmode(relpath, mode)
347
result = GioFileStream(self, relpath)
348
_file_streams[self.abspath(relpath)] = result
351
def recommended_page_size(self):
352
"""See Transport.recommended_page_size().
354
For FTP we suggest a large page size to reduce the overhead
355
introduced by latency.
357
if 'gio' in debug.debug_flags:
358
mutter("GIO recommended_page")
361
def rmdir(self, relpath):
362
"""Delete the directory at rel_path"""
364
if 'gio' in debug.debug_flags:
365
mutter("GIO rmdir %s" % relpath)
366
st = self.stat(relpath)
367
if stat.S_ISDIR(st.st_mode):
368
f = self._get_GIO(relpath)
371
raise errors.NotADirectory(relpath)
373
self._translate_gio_error(e, relpath)
374
except errors.NotADirectory, e:
375
#just pass it forward
378
mutter('failed to rmdir %s: %s' % (relpath, e))
379
raise errors.PathError(relpath)
381
def append_file(self, relpath, file, mode=None):
382
"""Append the text in the file-like object into the final
385
#GIO append_to seems not to append but to truncate
387
if 'gio' in debug.debug_flags:
388
mutter("GIO append_file: %s" % relpath)
389
tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
390
os.getpid(), random.randint(0, 0x7FFFFFFF))
393
fo = self._get_GIO(tmppath)
394
fi = self._get_GIO(relpath)
397
info = GioStatResult(fi)
398
result = info.st_size
400
self._pump(fin, fout)
402
#This separate except is to catch and ignore the
403
#gio.ERROR_NOT_FOUND for the already existing file.
404
#It is valid to open a non-existing file for append.
405
#This is caused by the broken gio append_to...
407
if e.code != gio.ERROR_NOT_FOUND:
408
self._translate_gio_error(e, relpath)
409
length = self._pump(file, fout)
411
info = GioStatResult(fo)
412
if info.st_size != result + length:
413
raise errors.BzrError("Failed to append size after " \
414
"(%d) is not original (%d) + written (%d) total (%d)" % \
415
(info.st_size, result, length, result + length))
416
fo.move(fi, flags=gio.FILE_COPY_OVERWRITE)
419
self._translate_gio_error(e, relpath)
421
def _setmode(self, relpath, mode):
422
"""Set permissions on a path.
424
Only set permissions on Unix systems
426
if 'gio' in debug.debug_flags:
427
mutter("GIO _setmode %s" % relpath)
430
f = self._get_GIO(relpath)
431
f.set_attribute_uint32(gio.FILE_ATTRIBUTE_UNIX_MODE, mode)
433
if e.code == gio.ERROR_NOT_SUPPORTED:
434
# Command probably not available on this server
435
mutter("GIO Could not set permissions to %s on %s. %s",
436
oct(mode), self._remote_path(relpath), str(e))
438
self._translate_gio_error(e, relpath)
440
def rename(self, rel_from, rel_to):
441
"""Rename without special overwriting"""
443
if 'gio' in debug.debug_flags:
444
mutter("GIO move (rename): %s => %s", rel_from, rel_to)
445
f = self._get_GIO(rel_from)
446
t = self._get_GIO(rel_to)
449
self._translate_gio_error(e, rel_from)
451
def move(self, rel_from, rel_to):
452
"""Move the item at rel_from to the location at rel_to"""
454
if 'gio' in debug.debug_flags:
455
mutter("GIO move: %s => %s", rel_from, rel_to)
456
f = self._get_GIO(rel_from)
457
t = self._get_GIO(rel_to)
458
f.move(t, flags=gio.FILE_COPY_OVERWRITE)
460
self._translate_gio_error(e, relfrom)
462
def delete(self, relpath):
463
"""Delete the item at relpath"""
465
if 'gio' in debug.debug_flags:
466
mutter("GIO delete: %s", relpath)
467
f = self._get_GIO(relpath)
470
self._translate_gio_error(e, relpath)
472
def external_url(self):
473
"""See bzrlib.transport.Transport.external_url."""
474
if 'gio' in debug.debug_flags:
475
mutter("GIO external_url", self.base)
480
"""See Transport.listable."""
481
if 'gio' in debug.debug_flags:
482
mutter("GIO listable")
485
def list_dir(self, relpath):
486
"""See Transport.list_dir."""
487
if 'gio' in debug.debug_flags:
488
mutter("GIO list_dir")
491
f = self._get_GIO(relpath)
492
children = f.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME)
493
for child in children:
494
entries.append(urlutils.escape(child.get_name()))
497
self._translate_gio_error(e, relpath)
499
def iter_files_recursive(self):
500
"""See Transport.iter_files_recursive.
502
This is cargo-culted from the SFTP transport"""
503
if 'gio' in debug.debug_flags:
504
mutter("GIO iter_files_recursive")
505
queue = list(self.list_dir("."))
507
relpath = queue.pop(0)
508
st = self.stat(relpath)
509
if stat.S_ISDIR(st.st_mode):
510
for i, basename in enumerate(self.list_dir(relpath)):
511
queue.insert(i, relpath + "/" + basename)
515
def stat(self, relpath):
516
"""Return the stat information for a file."""
518
if 'gio' in debug.debug_flags:
519
mutter("GIO stat: %s", relpath)
520
f = self._get_GIO(relpath)
521
return GioStatResult(f)
523
self._translate_gio_error(e, relpath, extra='error w/ stat')
525
def lock_read(self, relpath):
526
"""Lock the given file for shared (read) access.
527
:return: A lock object, which should be passed to Transport.unlock()
529
if 'gio' in debug.debug_flags:
530
mutter("GIO lock_read", relpath)
532
class BogusLock(object):
533
# The old RemoteBranch ignore lock for reading, so we will
534
# continue that tradition and return a bogus lock object.
536
def __init__(self, path):
542
return BogusLock(relpath)
544
def lock_write(self, relpath):
545
"""Lock the given file for exclusive (write) access.
546
WARNING: many transports do not support this, so trying avoid using it
548
:return: A lock object, whichshould be passed to Transport.unlock()
550
if 'gio' in debug.debug_flags:
551
mutter("GIO lock_write", relpath)
552
return self.lock_read(relpath)
554
def _translate_gio_error(self, err, path, extra=None):
555
if 'gio' in debug.debug_flags:
556
mutter("GIO Error: %s %s" % (str(err), path))
559
if err.code == gio.ERROR_NOT_FOUND:
560
raise errors.NoSuchFile(path, extra=extra)
561
elif err.code == gio.ERROR_EXISTS:
562
raise errors.FileExists(path, extra=extra)
563
elif err.code == gio.ERROR_NOT_DIRECTORY:
564
raise errors.NotADirectory(path, extra=extra)
565
elif err.code == gio.ERROR_NOT_EMPTY:
566
raise errors.DirectoryNotEmpty(path, extra=extra)
567
elif err.code == gio.ERROR_BUSY:
568
raise errors.ResourceBusy(path, extra=extra)
569
elif err.code == gio.ERROR_PERMISSION_DENIED:
570
raise errors.PermissionDenied(path, extra=extra)
571
elif err.code == gio.ERROR_HOST_NOT_FOUND:
572
raise errors.PathError(path, extra=extra)
573
elif err.code == gio.ERROR_IS_DIRECTORY:
574
raise errors.PathError(path, extra=extra)
576
mutter('unable to understand error for path: %s: %s', path, err)
577
raise errors.PathError(path,
578
extra="Unhandled gio error: " + str(err))
581
def get_test_permutations():
582
"""Return the permutations to be used in testing."""
583
from bzrlib.tests import test_server
584
return [(GioTransport, GioLocalURLServer)]