Merge lp:~snaggen/bzr/gio-transport into lp:bzr

Proposed by Mattias Eriksson on 2010-05-04
Status: Superseded
Proposed branch: lp:~snaggen/bzr/gio-transport
Merge into: lp:bzr
Diff against target: 600 lines (+585/-0)
2 files modified
bzrlib/transport/__init__.py (+3/-0)
bzrlib/transport/gio_transport.py (+582/-0)
To merge this branch: bzr merge lp:~snaggen/bzr/gio-transport
Reviewer Review Type Date Requested Status
John A Meinel 2010-05-13 Needs Fixing on 2010-05-24
Robert Collins (community) Needs Information on 2010-05-23
Parth Malwankar 2010-05-04 Abstain on 2010-05-12
Review via email: mp+24664@code.launchpad.net

This proposal has been superseded by a proposal from 2010-05-27.

Commit Message

Add a gio based transport using gio+ as a prefix to get at the gio logic.

Description of the Change

This adds a transport using the gnome lib gio for underlying operations. This means that it will be possible to use dav,smb,obex locations to store branches.

The current state is that the things pointed out by Martin Pool in the following mail has been fixed.
https://lists.ubuntu.com/archives/bazaar/2010q2/068374.html

The only outstanding issue is that one test case fails:
bzrlib.tests.per_transport.TransportTests.test_put_file_unicode(GioTransport,GioLocalURLServer)
AssertionError: UnicodeEncodeError not raised

This is due to the fact that the push_file method just uses the ConnectedTransport._pump method, which then just uses osutils.pumpfile. So the put_file method never reads the data and cant determine if the stream is unicode or not.

To post a comment you must log in.
Parth Malwankar (parthm) wrote :

Hi Mattias,

I don't know much about GIO but had a few general comments.

121 + #if 'gio' in debug.debug_flags:
122 + # mutter("stat size is %d bytes" % self.st_size)

329 + #if 'gio' in debug.debug_flags:
330 + # mutter("GIO put_file wrote %d bytes", length);

Maybe the above can be removed completely.

134 + self.mounted = 0
135 + """Set the base path where files will be stored."""
136 + if not base.startswith('gio+'):

The association of the string at 135 above is not very clear.

In the put_file function. I see there is a nested try with the outer one catching Exception and closing fout and f.
You could consider using just the inner try (with gio.Error) and closing these in a finally clause. Use of Exception is generally not a good idea as its a catch all.
Maybe something like:

try:
    # do processing
except gio.Error, e:
    # handle gio error
finally:
    if not closed and fout is not None:
        fout.close()
    if f is not None:
        f.delete()

For append_file. I noticed that both the nested try handle gio.Error. Is it possible to combine these into one. I also noticed that 'length' is defined inside the inner try and used outside. Can it happen that due to an exception, length is not defined and we try to use it outside?

Some lines are going beyond 80 chars, it would be good to break them. PEP 8[1] is used for coding style in bzr code base.

[1] http://www.python.org/dev/peps/pep-0008/

review: Needs Fixing
Martin Pool (mbp) wrote :

On 5 May 2010 14:24, Parth Malwankar <email address hidden> wrote:
> Review: Needs Fixing
> Hi Mattias,
>
> I don't know much about GIO but had a few general comments.
>
> 121    + #if 'gio' in debug.debug_flags:
> 122    + # mutter("stat size is %d bytes" % self.st_size)
>
> 329     + #if 'gio' in debug.debug_flags:
> 330     + # mutter("GIO put_file wrote %d bytes", length);
>
> Maybe the above can be removed completely.

Maybe they should be. My feeling is that people are welcome to leave
in debug-flag guarded mutter statements as long as they could possibly
be useful for someone to turn them on again when debugging a problem
in this area -- especially if you would ever ask a user to run with it
and show the output.

--
Martin <http://launchpad.net/~mbp/>

John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Martin Pool wrote:
> On 5 May 2010 14:24, Parth Malwankar <email address hidden> wrote:
>> Review: Needs Fixing
>> Hi Mattias,
>>
>> I don't know much about GIO but had a few general comments.
>>
>> 121 + #if 'gio' in debug.debug_flags:
>> 122 + # mutter("stat size is %d bytes" % self.st_size)
>>
>> 329 + #if 'gio' in debug.debug_flags:
>> 330 + # mutter("GIO put_file wrote %d bytes", length);
>>
>> Maybe the above can be removed completely.
>
> Maybe they should be. My feeling is that people are welcome to leave
> in debug-flag guarded mutter statements as long as they could possibly
> be useful for someone to turn them on again when debugging a problem
> in this area -- especially if you would ever ask a user to run with it
> and show the output.
>

Sure, but commented out debug flag code doesn't help anyone :).

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkvhha8ACgkQJdeBCYSNAAMOlACfawzLNCEZzQEmflE89l/Pn4KV
QzcAoNopBqe/s1JQLB6Tv7jCSPaOxIjB
=p5Gk
-----END PGP SIGNATURE-----

Mattias Eriksson (snaggen) wrote :

ons 2010-05-05 klockan 13:24 +0000 skrev Parth Malwankar:
> Review: Needs Fixing
> Hi Mattias,
>
> I don't know much about GIO but had a few general comments.
>
> 121 + #if 'gio' in debug.debug_flags:
> 122 + # mutter("stat size is %d bytes" % self.st_size)
>
> 329 + #if 'gio' in debug.debug_flags:
> 330 + # mutter("GIO put_file wrote %d bytes", length);
>
> Maybe the above can be removed completely.

Yes I'll remove those.

> 134 + self.mounted = 0
> 135 + """Set the base path where files will be stored."""
> 136 + if not base.startswith('gio+'):
>
> The association of the string at 135 above is not very clear.

I have changed the wording and moved some things to make it clearer

> In the put_file function. I see there is a nested try with the outer one catching Exception and closing fout and f.
> You could consider using just the inner try (with gio.Error) and closing these in a finally clause. Use of Exception is generally not a good idea as its a catch all.
> Maybe something like:
>
> try:
> # do processing
> except gio.Error, e:
> # handle gio error
> finally:
> if not closed and fout is not None:
> fout.close()
> if f is not None:
> f.delete()

Yes that is very much more readable... I didn't know about the finally
statement, that is very nice. Thanks for showing this.

> For append_file. I noticed that both the nested try handle gio.Error. Is it possible to combine these into one. I also noticed that 'length' is defined inside the inner try and used outside. Can it happen that due to an exception, length is not defined and we try to use it outside?

The length inside the inner try isn't used, so I removed that. length
(from the new part of the file) and the reslut from the stat operation
(on the already existing file) is used to verify that the append
operation is resulting in an expected file length.
And since the stat of the existing file may file with a ERROR_NOT_FOUND
which is legal I have the nested try to handle that and ignore that
error. This is due to the cludge I have to use since the gio append_to
is broken and corrupt files.

> Some lines are going beyond 80 chars, it would be good to break them. PEP 8[1] is used for coding style in bzr code base.

Fixed those.

//Mattias

Parth Malwankar (parthm) wrote :

Thanks Mattias. This looks good to me from a python perspective.
One minor tweak is that backslash (\) need not always be used to break lines. E.g. line breaks are handled cleanly when broken at comma in a function call. There are other implicit rules like this.

foo("a",
    "c",
    "d")

There is a good summary of line joining here:
http://google-styleguide.googlecode.com/svn/trunk/pyguide.html#Line_length

As I don't know much about GIO I will abstain from voting. Maybe someone else can give it a deeper look.

review: Abstain
John A Meinel (jameinel) wrote :

I think it would make the most sense to move "gio/__init__.py" to just "gio.py" since you don't have any other modules anyway.

248 + while self.mounted == 0:
249 + gtk.main_iteration(block=True)

^- We'd rather not import gtk if we can help it. Andrew Bennetts specifically pointed out it changes global state (not in good ways). Specifically:

>>> import sys
>>> print sys.getdefaultencoding()
'ascii'
>>> import gtk
>>> print sys.getdefaultencoding()
'utf-8'

And that affects any time you do unicode(str) [mostly implicitly] as it now treats those bytes as utf-8, rather than failing if it has non-ascii bytes. (We try pretty hard not to accidentally implicitly cast, but we get it wrong sometimes.)

Shouldn't there be an equivalent function to gtk.mainiteration() in gobject/etc?

I'm personally a little concerned that gio might get imported by accident (espec on platforms where it isn't available), but from what I can see you're doing it ok. We may need something like:

try:
  import gio
  import gtk # if we need it
except ImportError:
  raise errors.DependencyNotPresent(...)

We should have similar code in transport/sftp.py for paramiko.

Otherwise the test suite will probably fail on platforms where gio is not installed.

For testing the Authentication support, would it be possible to just use the FTPServer and subclass it slightly so that you access it via gio+ftp rather than just ftp://? You'd also want a check that the dependencies are present for ftp and for gio before running that permutation.

review: Resubmit
Mattias Eriksson (snaggen) wrote :

fre 2010-05-14 klockan 07:28 +0000 skrev John A Meinel:

> Review: Resubmit
> I think it would make the most sense to move "gio/__init__.py" to just "gio.py" since you don't have any other modules anyway.

Well the problem here is that if I move it from gio/__init__.py to just
gio.py I cant do "import gio" since we have a name clash. And it is
still to early for me to figure out a good name besides gio.py for my
module.

> 248 + while self.mounted == 0:
> 249 + gtk.main_iteration(block=True)
>
>
> ^- We'd rather not import gtk if we can help it. Andrew Bennetts specifically pointed out it changes global state (not in good ways). Specifically:
>
> >>> import sys
> >>> print sys.getdefaultencoding()
> 'ascii'
> >>> import gtk
> >>> print sys.getdefaultencoding()
> 'utf-8'
>
> And that affects any time you do unicode(str) [mostly implicitly] as it now treats those bytes as utf-8, rather than failing if it has non-ascii bytes. (We try pretty hard not to accidentally implicitly cast, but we get it wrong sometimes.)
>
> Shouldn't there be an equivalent function to gtk.mainiteration() in gobject/etc?

I didn't know that it caused problems, I have now removed gtk and
replaced this with running my own mainloop. That also had the benefit of
getting rid of my ugly while loop waiting for things to get done.

>
> I'm personally a little concerned that gio might get imported by accident (espec on platforms where it isn't available), but from what I can see you're doing it ok. We may need something like:
>
> try:
> import gio
> import gtk # if we need it
> except ImportError:
> raise errors.DependencyNotPresent(...)
>
> We should have similar code in transport/sftp.py for paramiko.
>
> Otherwise the test suite will probably fail on platforms where gio is not installed.

I initially didn't do this since it was handled very nice by default,
but I still want the test suite to work so I have added this.

> For testing the Authentication support, would it be possible to just use the FTPServer and subclass it slightly so that you access it via gio+ftp rather than just ftp://? You'd also want a check that the dependencies are present for ftp and for gio before running that permutation.

Well the problem was that I couldn't find any of the ftp server options
for lucid, so I wasn't able to get the ftp tests to run om my computer.

John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

...

> Well the problem here is that if I move it from gio/__init__.py to just
> gio.py I cant do "import gio" since we have a name clash. And it is
> still to early for me to figure out a good name besides gio.py for my
> module.

gio_transport.py comes to mind.

...

>> Shouldn't there be an equivalent function to gtk.mainiteration() in gobject/etc?
>
>
>
> I didn't know that it caused problems, I have now removed gtk and
> replaced this with running my own mainloop. That also had the benefit of
> getting rid of my ugly while loop waiting for things to get done.
>
>
...

>> For testing the Authentication support, would it be possible to just use the FTPServer and subclass it slightly so that you access it via gio+ftp rather than just ftp://? You'd also want a check that the dependencies are present for ftp and for gio before running that permutation.
>
>
> Well the problem was that I couldn't find any of the ftp server options
> for lucid, so I wasn't able to get the ftp tests to run om my computer.
>
>

I believe the python2.6 code requires:

  http://code.google.com/p/pyftpdlib/

(since that is maintained, vs the medusa server.)

I don't see a package for it, Vincent would have a better idea of what
it specifically takes to set up. (He has a babune slave running on
Lucid, I'm guessing he has pyftplib set up there.) I would guess it is a
download + setup.py install. Though not quite as nice as 'apt-get
install python-ftplib', it would at least be a start.

That said, if you've manually tested auth support, I'd be willing to
merge it. Likely someone like Vincent will come along later and make
sure it is properly tested :).

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkv0WaMACgkQJdeBCYSNAANcUwCfUCk6euSqQIPFrKaA/MnBlsZX
4KYAoLl6dH/w2vazBNr9p0K5j2ALRJxe
=HmJq
-----END PGP SIGNATURE-----

Mattias Eriksson (snaggen) wrote :

ons 2010-05-19 klockan 21:36 +0000 skrev John A Meinel:
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
>
> ...
>
>
> > Well the problem here is that if I move it from gio/__init__.py to just
> > gio.py I cant do "import gio" since we have a name clash. And it is
> > still to early for me to figure out a good name besides gio.py for my
> > module.
>
> gio_transport.py comes to mind.

That works for me... I had planed to fix it before I pushed yesterday,
but I forgot. I'll fix it.

Robert Collins (lifeless) wrote :

John, you voted resubmit - are you happy with the changes Mattias has made?

review: Needs Information
John A Meinel (jameinel) wrote :

218 + def _mount_done_cb(self, obj, res):
219 + try:
220 + obj.mount_enclosing_volume_finish(res)
221 + except gio.Error, e:
222 + print "ERROR: ", e
223 + finally:
224 + self.loop.quit()

^- This should at least be a trace.warning(), also try/except/finally isn't allowed in Python2.4, only 2.5+, so it will get rejected by PQM.

(Well, maybe, as the code may not get exercised since gio may not be installed there.)

Robert- do you know how to check if gio is available?

There is some concern about the potential to bit-rot if we don't have an auth test running, but as long as someone is actively using the code, that should get caught fairly quickly.

Overall, I think just handling the python2.4 issue, and it should be ok.

review: Needs Fixing
Mattias Eriksson (snaggen) wrote :

The print "ERROR is changed to raise a BzrError instead
The try/except/finally was recommended to me to make some code simpler. I didn't have it there at first. Anyway I removed that from the mount_done since it didn't really make any difference. The other place in the code requires more job.

Parth Malwankar (parthm) wrote :

> The print "ERROR is changed to raise a BzrError instead
> The try/except/finally was recommended to me to make some code simpler. I

Sorry about that. I wasn't aware that 2.4 only supported try/finally and
not try/except/finally. :)

> didn't have it there at first. Anyway I removed that from the mount_done since
> it didn't really make any difference. The other place in the code requires
> more job.

Robert Collins (lifeless) wrote :

sent to pqm by email

Martin Pool (mbp) wrote :

On 26 May 2010 18:30, Parth Malwankar <email address hidden> wrote:
>> The print "ERROR is changed to raise a BzrError instead
>> The try/except/finally was recommended to me to make some code simpler. I
>
> Sorry about that. I wasn't aware that 2.4 only supported try/finally and
> not try/except/finally. :)

np, https://code.edge.launchpad.net/~mbp/bzr/doc/+merge/26113

please suggest anything else that comes up too.
--
Martin <http://launchpad.net/~mbp/>

Parth Malwankar (parthm) wrote :

As this is a user visible change it should probably have a NEWS entry under 'New Features'.

Mattias Eriksson (snaggen) wrote :

I just fixed the try/catch/finally issue pointed out by John. Also added a NEWS entry.

Parth Malwankar (parthm) wrote :

Hi Mattias,

The new changes don't seem to be visible in the diff. Could you push the latest changes and set the status to "Needs Review".
The current diff has already been landed on the mainline.

Parth Malwankar (parthm) wrote :

> Hi Mattias,
>
> The new changes don't seem to be visible in the diff. Could you push the
> latest changes and set the status to "Needs Review".
> The current diff has already been landed on the mainline.

Never mind. It seems that merged branches are not scanned. I have set it to needs review for the diff.

lp:~snaggen/bzr/gio-transport updated on 2010-05-28
5221. By Mattias Eriksson on 2010-05-28

Merge bzr.dev

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/transport/__init__.py'
2--- bzrlib/transport/__init__.py 2010-05-04 06:22:51 +0000
3+++ bzrlib/transport/__init__.py 2010-05-25 09:19:27 +0000
4@@ -1743,6 +1743,9 @@
5 register_transport_proto('aftp://', help="Access using active FTP.")
6 register_lazy_transport('aftp://', 'bzrlib.transport.ftp', 'FtpTransport')
7
8+register_transport_proto('gio+', help="Access using any GIO supported protocols.")
9+register_lazy_transport('gio+', 'bzrlib.transport.gio_transport', 'GioTransport')
10+
11 try:
12 import kerberos
13 kerberos_available = True
14
15=== added file 'bzrlib/transport/gio_transport.py'
16--- bzrlib/transport/gio_transport.py 1970-01-01 00:00:00 +0000
17+++ bzrlib/transport/gio_transport.py 2010-05-25 09:19:27 +0000
18@@ -0,0 +1,582 @@
19+# Copyright (C) 2010 Canonical Ltd.
20+#
21+# This program is free software; you can redistribute it and/or modify
22+# it under the terms of the GNU General Public License as published by
23+# the Free Software Foundation; either version 2 of the License, or
24+# (at your option) any later version.
25+#
26+# This program is distributed in the hope that it will be useful,
27+# but WITHOUT ANY WARRANTY; without even the implied warranty of
28+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29+# GNU General Public License for more details.
30+#
31+# You should have received a copy of the GNU General Public License
32+# along with this program; if not, write to the Free Software
33+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
34+#
35+# Author: Mattias Eriksson
36+
37+"""Implementation of Transport over gio.
38+
39+Written by Mattias Eriksson <snaggen@acc.umu.se> based on the ftp transport.
40+
41+It provides the gio+XXX:// protocols where XXX is any of the protocols
42+supported by gio.
43+"""
44+from cStringIO import StringIO
45+import getpass
46+import os
47+import random
48+import socket
49+import stat
50+import urllib
51+import time
52+import sys
53+import getpass
54+import urlparse
55+
56+from bzrlib import (
57+ config,
58+ errors,
59+ osutils,
60+ urlutils,
61+ debug,
62+ ui,
63+ )
64+from bzrlib.trace import mutter, warning
65+from bzrlib.transport import (
66+ FileStream,
67+ ConnectedTransport,
68+ _file_streams,
69+ Server,
70+ )
71+
72+from bzrlib.tests.test_server import TestServer
73+
74+try:
75+ import glib
76+except ImportError, e:
77+ raise errors.DependencyNotPresent('glib', e)
78+try:
79+ import gio
80+except ImportError, e:
81+ raise errors.DependencyNotPresent('gio', e)
82+
83+
84+class GioLocalURLServer(TestServer):
85+ """A pretend server for local transports, using file:// urls.
86+
87+ Of course no actual server is required to access the local filesystem, so
88+ this just exists to tell the test code how to get to it.
89+ """
90+
91+ def start_server(self):
92+ pass
93+
94+ def get_url(self):
95+ """See Transport.Server.get_url."""
96+ return "gio+" + urlutils.local_path_to_url('')
97+
98+
99+class GioFileStream(FileStream):
100+ """A file stream object returned by open_write_stream.
101+
102+ This version uses GIO to perform writes.
103+ """
104+
105+ def __init__(self, transport, relpath):
106+ FileStream.__init__(self, transport, relpath)
107+ self.gio_file = transport._get_GIO(relpath)
108+ self.stream = self.gio_file.create()
109+
110+ def _close(self):
111+ self.stream.close()
112+
113+ def write(self, bytes):
114+ try:
115+ #Using pump_string_file seems to make things crash
116+ osutils.pumpfile(StringIO(bytes), self.stream)
117+ except gio.Error, e:
118+ #self.transport._translate_gio_error(e,self.relpath)
119+ raise errors.BzrError(str(e))
120+
121+
122+class GioStatResult(object):
123+
124+ def __init__(self, f):
125+ info = f.query_info('standard::size,standard::type')
126+ self.st_size = info.get_size()
127+ type = info.get_file_type()
128+ if (type == gio.FILE_TYPE_REGULAR):
129+ self.st_mode = stat.S_IFREG
130+ elif type == gio.FILE_TYPE_DIRECTORY:
131+ self.st_mode = stat.S_IFDIR
132+
133+
134+class GioTransport(ConnectedTransport):
135+ """This is the transport agent for gio+XXX:// access."""
136+
137+ def __init__(self, base, _from_transport=None):
138+ """Initialize the GIO transport and make sure the url is correct."""
139+
140+ if not base.startswith('gio+'):
141+ raise ValueError(base)
142+
143+ (scheme, netloc, path, params, query, fragment) = \
144+ urlparse.urlparse(base[len('gio+'):], allow_fragments=False)
145+ if '@' in netloc:
146+ user, netloc = netloc.rsplit('@', 1)
147+ #Seems it is not possible to list supported backends for GIO
148+ #so a hardcoded list it is then.
149+ gio_backends = ['dav', 'file', 'ftp', 'obex', 'sftp', 'ssh', 'smb']
150+ if scheme not in gio_backends:
151+ raise errors.InvalidURL(base,
152+ extra="GIO support is only available for " + \
153+ ', '.join(gio_backends))
154+
155+ #Remove the username and password from the url we send to GIO
156+ #by rebuilding the url again.
157+ u = (scheme, netloc, path, '', '', '')
158+ self.url = urlparse.urlunparse(u)
159+
160+ # And finally initialize super
161+ super(GioTransport, self).__init__(base,
162+ _from_transport=_from_transport)
163+
164+ def _relpath_to_url(self, relpath):
165+ full_url = urlutils.join(self.url, relpath)
166+ if isinstance(full_url, unicode):
167+ raise errors.InvalidURL(full_url)
168+ return full_url
169+
170+ def _get_GIO(self, relpath):
171+ """Return the ftplib.GIO instance for this object."""
172+ # Ensures that a connection is established
173+ connection = self._get_connection()
174+ if connection is None:
175+ # First connection ever
176+ connection, credentials = self._create_connection()
177+ self._set_connection(connection, credentials)
178+ fileurl = self._relpath_to_url(relpath)
179+ file = gio.File(fileurl)
180+ return file
181+
182+ def _auth_cb(self, op, message, default_user, default_domain, flags):
183+ #really use bzrlib.auth get_password for this
184+ #or possibly better gnome-keyring?
185+ auth = config.AuthenticationConfig()
186+ (scheme, urluser, urlpassword, host, port, urlpath) = \
187+ urlutils.parse_url(self.url)
188+ user = None
189+ if (flags & gio.ASK_PASSWORD_NEED_USERNAME and
190+ flags & gio.ASK_PASSWORD_NEED_DOMAIN):
191+ prompt = scheme.upper() + ' %(host)s DOMAIN\username'
192+ user_and_domain = auth.get_user(scheme, host,
193+ port=port, ask=True, prompt=prompt)
194+ (domain, user) = user_and_domain.split('\\', 1)
195+ op.set_username(user)
196+ op.set_domain(domain)
197+ elif flags & gio.ASK_PASSWORD_NEED_USERNAME:
198+ user = auth.get_user(scheme, host,
199+ port=port, ask=True)
200+ op.set_username(user)
201+ elif flags & gio.ASK_PASSWORD_NEED_DOMAIN:
202+ #Don't know how common this case is, but anyway
203+ #a DOMAIN and a username prompt should be the
204+ #same so I will missuse the ui_factory get_username
205+ #a little bit here.
206+ prompt = scheme.upper() + ' %(host)s DOMAIN'
207+ domain = ui.ui_factory.get_username(prompt=prompt)
208+ op.set_domain(domain)
209+
210+ if flags & gio.ASK_PASSWORD_NEED_PASSWORD:
211+ if user is None:
212+ user = op.get_username()
213+ password = auth.get_password(scheme, host,
214+ user, port=port)
215+ op.set_password(password)
216+ op.reply(gio.MOUNT_OPERATION_HANDLED)
217+
218+ def _mount_done_cb(self, obj, res):
219+ try:
220+ obj.mount_enclosing_volume_finish(res)
221+ self.loop.quit()
222+ except gio.Error, e:
223+ self.loop.quit()
224+ raise errors.BzrError("Failed to mount the given location: " + str(e));
225+
226+ def _create_connection(self, credentials=None):
227+ if credentials is None:
228+ user, password = self._user, self._password
229+ else:
230+ user, password = credentials
231+
232+ try:
233+ connection = gio.File(self.url)
234+ mount = None
235+ try:
236+ mount = connection.find_enclosing_mount()
237+ except gio.Error, e:
238+ if (e.code == gio.ERROR_NOT_MOUNTED):
239+ self.loop = glib.MainLoop()
240+ ui.ui_factory.show_message('Mounting %s using GIO' % \
241+ self.url)
242+ op = gio.MountOperation()
243+ if user:
244+ op.set_username(user)
245+ if password:
246+ op.set_password(password)
247+ op.connect('ask-password', self._auth_cb)
248+ m = connection.mount_enclosing_volume(op,
249+ self._mount_done_cb)
250+ self.loop.run()
251+ except gio.Error, e:
252+ raise errors.TransportError(msg="Error setting up connection:"
253+ " %s" % str(e), orig_error=e)
254+ return connection, (user, password)
255+
256+ def _reconnect(self):
257+ """Create a new connection with the previously used credentials"""
258+ credentials = self._get_credentials()
259+ connection, credentials = self._create_connection(credentials)
260+ self._set_connection(connection, credentials)
261+
262+ def _remote_path(self, relpath):
263+ relative = urlutils.unescape(relpath).encode('utf-8')
264+ remote_path = self._combine_paths(self._path, relative)
265+ return remote_path
266+
267+ def has(self, relpath):
268+ """Does the target location exist?"""
269+ try:
270+ if 'gio' in debug.debug_flags:
271+ mutter('GIO has check: %s' % relpath)
272+ f = self._get_GIO(relpath)
273+ st = GioStatResult(f)
274+ if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
275+ return True
276+ return False
277+ except gio.Error, e:
278+ if e.code == gio.ERROR_NOT_FOUND:
279+ return False
280+ else:
281+ self._translate_gio_error(e, relpath)
282+
283+ def get(self, relpath, decode=False, retries=0):
284+ """Get the file at the given relative path.
285+
286+ :param relpath: The relative path to the file
287+ :param retries: Number of retries after temporary failures so far
288+ for this operation.
289+
290+ We're meant to return a file-like object which bzr will
291+ then read from. For now we do this via the magic of StringIO
292+ """
293+ try:
294+ if 'gio' in debug.debug_flags:
295+ mutter("GIO get: %s" % relpath)
296+ f = self._get_GIO(relpath)
297+ fin = f.read()
298+ buf = fin.read()
299+ fin.close()
300+ ret = StringIO(buf)
301+ return ret
302+ except gio.Error, e:
303+ #If we get a not mounted here it might mean
304+ #that a bad path has been entered (or that mount failed)
305+ if (e.code == gio.ERROR_NOT_MOUNTED):
306+ raise errors.PathError(relpath,
307+ extra='Failed to get file, make sure the path is correct. ' \
308+ + str(e))
309+ else:
310+ self._translate_gio_error(e, relpath)
311+
312+ def put_file(self, relpath, fp, mode=None):
313+ """Copy the file-like object into the location.
314+
315+ :param relpath: Location to put the contents, relative to base.
316+ :param fp: File-like or string object.
317+ """
318+ if 'gio' in debug.debug_flags:
319+ mutter("GIO put_file %s" % relpath)
320+ tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
321+ os.getpid(), random.randint(0, 0x7FFFFFFF))
322+ f = None
323+ fout = None
324+ try:
325+ f = self._get_GIO(tmppath)
326+ fout = f.create()
327+ closed = False
328+ length = self._pump(fp, fout)
329+ fout.close()
330+ closed = True
331+ self.stat(tmppath)
332+ dest = self._get_GIO(relpath)
333+ f.move(dest, flags=gio.FILE_COPY_OVERWRITE)
334+ f = None
335+ if mode is not None:
336+ self._setmode(relpath, mode)
337+ return length
338+ except gio.Error, e:
339+ self._translate_gio_error(e, relpath)
340+ finally:
341+ if not closed and fout is not None:
342+ fout.close()
343+ if f is not None and f.query_exists():
344+ f.delete()
345+
346+ def mkdir(self, relpath, mode=None):
347+ """Create a directory at the given path."""
348+ try:
349+ if 'gio' in debug.debug_flags:
350+ mutter("GIO mkdir: %s" % relpath)
351+ f = self._get_GIO(relpath)
352+ f.make_directory()
353+ self._setmode(relpath, mode)
354+ except gio.Error, e:
355+ self._translate_gio_error(e, relpath)
356+
357+ def open_write_stream(self, relpath, mode=None):
358+ """See Transport.open_write_stream."""
359+ if 'gio' in debug.debug_flags:
360+ mutter("GIO open_write_stream %s" % relpath)
361+ if mode is not None:
362+ self._setmode(relpath, mode)
363+ result = GioFileStream(self, relpath)
364+ _file_streams[self.abspath(relpath)] = result
365+ return result
366+
367+ def recommended_page_size(self):
368+ """See Transport.recommended_page_size().
369+
370+ For FTP we suggest a large page size to reduce the overhead
371+ introduced by latency.
372+ """
373+ if 'gio' in debug.debug_flags:
374+ mutter("GIO recommended_page")
375+ return 64 * 1024
376+
377+ def rmdir(self, relpath):
378+ """Delete the directory at rel_path"""
379+ try:
380+ if 'gio' in debug.debug_flags:
381+ mutter("GIO rmdir %s" % relpath)
382+ st = self.stat(relpath)
383+ if stat.S_ISDIR(st.st_mode):
384+ f = self._get_GIO(relpath)
385+ f.delete()
386+ else:
387+ raise errors.NotADirectory(relpath)
388+ except gio.Error, e:
389+ self._translate_gio_error(e, relpath)
390+ except errors.NotADirectory, e:
391+ #just pass it forward
392+ raise e
393+ except Exception, e:
394+ mutter('failed to rmdir %s: %s' % (relpath, e))
395+ raise errors.PathError(relpath)
396+
397+ def append_file(self, relpath, file, mode=None):
398+ """Append the text in the file-like object into the final
399+ location.
400+ """
401+ #GIO append_to seems not to append but to truncate
402+ #Work around this.
403+ if 'gio' in debug.debug_flags:
404+ mutter("GIO append_file: %s" % relpath)
405+ tmppath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
406+ os.getpid(), random.randint(0, 0x7FFFFFFF))
407+ try:
408+ result = 0
409+ fo = self._get_GIO(tmppath)
410+ fi = self._get_GIO(relpath)
411+ fout = fo.create()
412+ try:
413+ info = GioStatResult(fi)
414+ result = info.st_size
415+ fin = fi.read()
416+ self._pump(fin, fout)
417+ fin.close()
418+ #This separate except is to catch and ignore the
419+ #gio.ERROR_NOT_FOUND for the already existing file.
420+ #It is valid to open a non-existing file for append.
421+ #This is caused by the broken gio append_to...
422+ except gio.Error, e:
423+ if e.code != gio.ERROR_NOT_FOUND:
424+ self._translate_gio_error(e, relpath)
425+ length = self._pump(file, fout)
426+ fout.close()
427+ info = GioStatResult(fo)
428+ if info.st_size != result + length:
429+ raise errors.BzrError("Failed to append size after " \
430+ "(%d) is not original (%d) + written (%d) total (%d)" % \
431+ (info.st_size, result, length, result + length))
432+ fo.move(fi, flags=gio.FILE_COPY_OVERWRITE)
433+ return result
434+ except gio.Error, e:
435+ self._translate_gio_error(e, relpath)
436+
437+ def _setmode(self, relpath, mode):
438+ """Set permissions on a path.
439+
440+ Only set permissions on Unix systems
441+ """
442+ if 'gio' in debug.debug_flags:
443+ mutter("GIO _setmode %s" % relpath)
444+ if mode:
445+ try:
446+ f = self._get_GIO(relpath)
447+ f.set_attribute_uint32(gio.FILE_ATTRIBUTE_UNIX_MODE, mode)
448+ except gio.Error, e:
449+ if e.code == gio.ERROR_NOT_SUPPORTED:
450+ # Command probably not available on this server
451+ mutter("GIO Could not set permissions to %s on %s. %s",
452+ oct(mode), self._remote_path(relpath), str(e))
453+ else:
454+ self._translate_gio_error(e, relpath)
455+
456+ def rename(self, rel_from, rel_to):
457+ """Rename without special overwriting"""
458+ try:
459+ if 'gio' in debug.debug_flags:
460+ mutter("GIO move (rename): %s => %s", rel_from, rel_to)
461+ f = self._get_GIO(rel_from)
462+ t = self._get_GIO(rel_to)
463+ f.move(t)
464+ except gio.Error, e:
465+ self._translate_gio_error(e, rel_from)
466+
467+ def move(self, rel_from, rel_to):
468+ """Move the item at rel_from to the location at rel_to"""
469+ try:
470+ if 'gio' in debug.debug_flags:
471+ mutter("GIO move: %s => %s", rel_from, rel_to)
472+ f = self._get_GIO(rel_from)
473+ t = self._get_GIO(rel_to)
474+ f.move(t, flags=gio.FILE_COPY_OVERWRITE)
475+ except gio.Error, e:
476+ self._translate_gio_error(e, relfrom)
477+
478+ def delete(self, relpath):
479+ """Delete the item at relpath"""
480+ try:
481+ if 'gio' in debug.debug_flags:
482+ mutter("GIO delete: %s", relpath)
483+ f = self._get_GIO(relpath)
484+ f.delete()
485+ except gio.Error, e:
486+ self._translate_gio_error(e, relpath)
487+
488+ def external_url(self):
489+ """See bzrlib.transport.Transport.external_url."""
490+ if 'gio' in debug.debug_flags:
491+ mutter("GIO external_url", self.base)
492+ # GIO external url
493+ return self.base
494+
495+ def listable(self):
496+ """See Transport.listable."""
497+ if 'gio' in debug.debug_flags:
498+ mutter("GIO listable")
499+ return True
500+
501+ def list_dir(self, relpath):
502+ """See Transport.list_dir."""
503+ if 'gio' in debug.debug_flags:
504+ mutter("GIO list_dir")
505+ try:
506+ entries = []
507+ f = self._get_GIO(relpath)
508+ children = f.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME)
509+ for child in children:
510+ entries.append(urlutils.escape(child.get_name()))
511+ return entries
512+ except gio.Error, e:
513+ self._translate_gio_error(e, relpath)
514+
515+ def iter_files_recursive(self):
516+ """See Transport.iter_files_recursive.
517+
518+ This is cargo-culted from the SFTP transport"""
519+ if 'gio' in debug.debug_flags:
520+ mutter("GIO iter_files_recursive")
521+ queue = list(self.list_dir("."))
522+ while queue:
523+ relpath = queue.pop(0)
524+ st = self.stat(relpath)
525+ if stat.S_ISDIR(st.st_mode):
526+ for i, basename in enumerate(self.list_dir(relpath)):
527+ queue.insert(i, relpath + "/" + basename)
528+ else:
529+ yield relpath
530+
531+ def stat(self, relpath):
532+ """Return the stat information for a file."""
533+ try:
534+ if 'gio' in debug.debug_flags:
535+ mutter("GIO stat: %s", relpath)
536+ f = self._get_GIO(relpath)
537+ return GioStatResult(f)
538+ except gio.Error, e:
539+ self._translate_gio_error(e, relpath, extra='error w/ stat')
540+
541+ def lock_read(self, relpath):
542+ """Lock the given file for shared (read) access.
543+ :return: A lock object, which should be passed to Transport.unlock()
544+ """
545+ if 'gio' in debug.debug_flags:
546+ mutter("GIO lock_read", relpath)
547+
548+ class BogusLock(object):
549+ # The old RemoteBranch ignore lock for reading, so we will
550+ # continue that tradition and return a bogus lock object.
551+
552+ def __init__(self, path):
553+ self.path = path
554+
555+ def unlock(self):
556+ pass
557+
558+ return BogusLock(relpath)
559+
560+ def lock_write(self, relpath):
561+ """Lock the given file for exclusive (write) access.
562+ WARNING: many transports do not support this, so trying avoid using it
563+
564+ :return: A lock object, whichshould be passed to Transport.unlock()
565+ """
566+ if 'gio' in debug.debug_flags:
567+ mutter("GIO lock_write", relpath)
568+ return self.lock_read(relpath)
569+
570+ def _translate_gio_error(self, err, path, extra=None):
571+ if 'gio' in debug.debug_flags:
572+ mutter("GIO Error: %s %s" % (str(err), path))
573+ if extra is None:
574+ extra = str(err)
575+ if err.code == gio.ERROR_NOT_FOUND:
576+ raise errors.NoSuchFile(path, extra=extra)
577+ elif err.code == gio.ERROR_EXISTS:
578+ raise errors.FileExists(path, extra=extra)
579+ elif err.code == gio.ERROR_NOT_DIRECTORY:
580+ raise errors.NotADirectory(path, extra=extra)
581+ elif err.code == gio.ERROR_NOT_EMPTY:
582+ raise errors.DirectoryNotEmpty(path, extra=extra)
583+ elif err.code == gio.ERROR_BUSY:
584+ raise errors.ResourceBusy(path, extra=extra)
585+ elif err.code == gio.ERROR_PERMISSION_DENIED:
586+ raise errors.PermissionDenied(path, extra=extra)
587+ elif err.code == gio.ERROR_HOST_NOT_FOUND:
588+ raise errors.PathError(path, extra=extra)
589+ elif err.code == gio.ERROR_IS_DIRECTORY:
590+ raise errors.PathError(path, extra=extra)
591+ else:
592+ mutter('unable to understand error for path: %s: %s', path, err)
593+ raise errors.PathError(path,
594+ extra="Unhandled gio error: " + str(err))
595+
596+
597+def get_test_permutations():
598+ """Return the permutations to be used in testing."""
599+ from bzrlib.tests import test_server
600+ return [(GioTransport, GioLocalURLServer)]