Merge lp:~rls/bzr/transport-link-and-symlink-support into lp:bzr

Proposed by Neil Santos
Status: Merged
Approved by: Martin Pool
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~rls/bzr/transport-link-and-symlink-support
Merge into: lp:bzr
Diff against target: 181 lines (+111/-4)
4 files modified
bzrlib/tests/per_transport.py (+46/-0)
bzrlib/transport/__init__.py (+13/-1)
bzrlib/transport/local.py (+28/-1)
bzrlib/transport/sftp.py (+24/-2)
To merge this branch: bzr merge lp:~rls/bzr/transport-link-and-symlink-support
Reviewer Review Type Date Requested Status
Martin Pool Approve
Neil Santos (community) Approve
Martin Packman (community) Approve
Review via email: mp+20620@code.launchpad.net

This proposal supersedes a proposal from 2010-02-25.

To post a comment you must log in.
Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

Attempt to include support for hard- and symlinks into the Transport classes that advertise support for one or both them. Needed in the mainline to more cleanly support links for (in particular) the bzr-upload plugin.

Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

I'd prefer this style:

+ if osutils.hardlinks_good()
+ def link(self, source, link_name):
...

Over the current:

+ def link(self, source, link_name):
...
+ except AttributeError:
+ raise errors.TransportNotPossible("Hardlinks are not supported on %s" % self)

review: Needs Fixing
Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

I've been wondering whether I should implement link() for LocalTransport using osutil's link_or_copy(); I figured it's what it's there for. Is there any reason why I shouldn't do that?

review: Needs Information
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

For clarity (especially for non unix programmers) I might s/link/hardlink

On 26 February 2010 02:50, Neil Santos <email address hidden> wrote:
> Review: Needs Information
> I've been wondering whether I should implement link() for LocalTransport using osutil's link_or_copy(); I figured it's what it's there for.  Is there any reason why I shouldn't do that?

That seems a bit like an abstraction inversion. If it's desirable
(sometimes?) to copy when you can't hardlink, then that should be done
across all transports. I don't think you would want this
unconditionally, so it should be either a different transport method
or an option to .hardlink(). Making it a separate hardlink_or_copy()
would allow it to be defined in the Transport base class without
needing to be overridden per transport.

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

Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

> I'd prefer this style:
>
> + if osutils.hardlinks_good()
> + def link(self, source, link_name):
> ...

Implemented. There's another problem I've ran into (and have bothered mbp with): the tests for SFTPTransport's symlink() implementation raises an IOError (Operation unsupported).

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal
Download full text (6.6 KiB)

I think this is pretty close; thanks for posting it. Just a few comments:

=== modified file 'bzrlib/tests/per_transport.py'
--- bzrlib/tests/per_transport.py 2010-02-23 07:43:11 +0000
+++ bzrlib/tests/per_transport.py 2010-02-26 04:50:30 +0000
@@ -1083,6 +1083,52 @@
         subdir.stat('./file')
         subdir.stat('.')

+ def test_link(self):
+ from stat import ST_NLINK
+
+ t = self.get_transport()
+
+ source_name = "original_target"
+ link_name = "target_link"
+
+ self.build_tree([source_name], transport=t)
+
+ try:
+ link_result = t.link(source_name, link_name)
+ self.failUnless(link_result)

Returning True for success is a bit strange in Python. Why not just
rely on exceptions to indicate errors?

+
+ self.failUnless(t.has(source_name))
+ self.failUnless(t.has(link_name))
+
+ st = t.stat(link_name)
+ self.failUnlessEqual(st[ST_NLINK], 2)

Good; I think there might be a TestCase method to check they're linked
to each other.

+ except TransportNotPossible:
+ # This transport doesn't do hardlinks
+ return

Probably better to indicate it was skipped: raise TestSkipped(...).

+
+ def test_symlink(self):
+ from stat import S_ISLNK
+
+ t = self.get_transport()
+
+ source_name = "original_target"
+ link_name = "target_link"
+
+ self.build_tree([source_name], transport=t)
+
+ try:
+ link_result = t.symlink(source_name, link_name)
+ self.failUnless(link_result)
+

likewise

+ self.failUnless(t.has(source_name))
+ self.failUnless(t.has(link_name))
+
+ st = t.stat(link_name)
+ self.failUnless(S_ISLNK(st.st_mode))

I think you should actually read the link and check it had what you
expected.

+ except TransportNotPossible:
+ # This transport doesn't do hardlinks
+ return
+
     def test_list_dir(self):
         # TODO: Test list_dir, just try once, and if it throws, stop testing
         t = self.get_transport()

=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py 2010-02-23 07:43:11 +0000
+++ bzrlib/transport/__init__.py 2010-02-26 04:50:30 +0000
@@ -1202,6 +1202,20 @@
         count = self._iterate_over(relpaths, gather, pb, 'stat', expand=False)
         return stats

+ def link(self, source, link_name):
+ """Create a hardlink pointing to source named link_name.
+
+ Return True if successful.
+ """
+ raise errors.TransportNotPossible("Hard links are not supported on %s" % self)
+
+ def symlink(self, source, link_name):
+ """Create a symlink pointing to source named link_name.
+
+ Return True if successful.
+ """
+ raise errors.TransportNotPossible("Symlinks are not supported on %s" % self)
+
     def listable(self):
         """Return True if this store supports listing."""
         raise NotImplementedError(self.listable)

=== modified file 'bzrlib/transport/local.py'
--- bzrlib/transport/local.py 2010-02-23 07:43:11 +0000
+++ bzrlib/transport/local.py 2010-02-26 04:50:30 +0...

Read more...

review: Needs Fixing
Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

> Returning True for success is a bit strange in Python. Why not just
> rely on exceptions to indicate errors?

Agreed. I did it that way mostly because Paramiko's symlink() returns constants, and I wasn't entirely which way to go, and figured one way's as good as the other. I'm scouring bzrlib/errors.py for an appropriate exception to throw, and would be grateful if you could point out the one most useful in this case.

> + st = t.stat(link_name)
> + self.failUnlessEqual(st[ST_NLINK], 2)
>
> Good; I think there might be a TestCase method to check they're linked
> to each other.

Okay; will go look for it.

> + except TransportNotPossible:
> + # This transport doesn't do hardlinks
> + return
>
> Probably better to indicate it was skipped: raise TestSkipped(...).

Changing; did it that way because that's how test_stat() did it. :P

> + st = t.stat(link_name)
> + self.failUnless(S_ISLNK(st.st_mode))
>
> I think you should actually read the link and check it had what you
> expected.

Understood.

> This is in LocalTransport.stat, obviously changing it to behave more
> like lstat that stat. Since transports never previously expected to
> see symlinks perhaps it will not break anything, but on the other hand
> perhaps it is a bit surprising that t.stat behaves like lstat. If we
> ever actually wanted to follow symlinks, we'd need to add another
> method for that.
>
> Perhaps if that behaviour is changed it should be tested.
>
> Alternatively you could add a t.lstat.

I originally wanted to add an lstat(), though I figured I should just get it mostly working from the get-go, and check with reviewers to see which way it should go. I'll slap on an lstat() where appropriate and change stat() back to how it was, and see where that leads us.

> I posted an rfc. On the whole I think what you have is fine.

Thanks! :)

> Similarly to what gzlist said, I think here you should do
>
> try:
> link = os.link
> except AttributeError, ...
> raise TransportNotPossible(...)
>
> link(...)

Do you mean this:

    if osutils.hardlinks_good():
        def link(self, source, link_name):
            """See Transport.link."""
            try:
                link = os.link
            except AttributeError:
                return errors.TransportNotPossible("Hardlinks are not supported on %s" %self)
            except (IOError, OSError), e:
                self._translate_error(e, source)

            link(self._abspath(source), self._abspath(link_name))

Or just straight up declaring a member named 'link' as an alias to os.link()?

Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

> + st = t.stat(link_name)
> + self.failUnlessEqual(st[ST_NLINK], 2)
>
> Good; I think there might be a TestCase method to check they're linked
> to each other.

Hrm... I didn't find such a method per se, but I didn't find a couple of tests using osutils' readlink(), which should be good enough for LocalTransport, as well as Paramiko's SFTP's readlink(). I'll go ahead and add a readlink() to Transport; let me know how it looks.

Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

> > I'd prefer this style:
> >
> > + if osutils.hardlinks_good()
> > + def link(self, source, link_name):
> > ...
>
> Implemented. ...

Ah, I also meant this to go for the `symlink` function on the same object, with `osutils.has_symlinks` as the definition-time check. The point being, you know what the module is loaded if the python implementation has a (?:sym)link function, so don't need to check within the call itself. You do still need to check for other errors, as the specific filesystem/path may not support the given operation.

Not sure what the main Martin meant with his code fragment on the same topic though.

Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

> Ah, I also meant this to go for the `symlink` function on the same object,
> with `osutils.has_symlinks` as the definition-time check. The point being, you

Ah, LOL. I also figured I should do this; in fact, my last commit contains this, as well as a similar guard for the readlink() I'm implementing (using osutils.host_os_dereferences_symlink()).

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> > Returning True for success is a bit strange in Python. Why not just
> > rely on exceptions to indicate errors?
>
> Agreed. I did it that way mostly because Paramiko's symlink() returns
> constants, and I wasn't entirely which way to go, and figured one way's as
> good as the other. I'm scouring bzrlib/errors.py for an appropriate exception
> to throw, and would be grateful if you could point out the one most useful in
> this case.

If paramiko returns SFTP_OK then just return (which implicitly returns None.)

If it returns an error you don't specifically know what to do with, it would be reasonable to raise just a generic TransportError with the error code and path. However it seems like paramiko normally translates errors into exceptions itself, so you may not need to worry about this?

> > This is in LocalTransport.stat, obviously changing it to behave more
> > like lstat that stat. Since transports never previously expected to
> > see symlinks perhaps it will not break anything, but on the other hand
> > perhaps it is a bit surprising that t.stat behaves like lstat. If we
> > ever actually wanted to follow symlinks, we'd need to add another
> > method for that.
> >
> > Perhaps if that behaviour is changed it should be tested.
> >
> > Alternatively you could add a t.lstat.
>
> I originally wanted to add an lstat(), though I figured I should just get it
> mostly working from the get-go, and check with reviewers to see which way it
> should go. I'll slap on an lstat() where appropriate and change stat() back
> to how it was, and see where that leads us.

Actually the thread seemed to conclude that it was better for Transport.stat() to call os.lstat, and then make the higher level code specifically deal with the symlink.

>
> > I posted an rfc. On the whole I think what you have is fine.
>
> Thanks! :)
>
> > Similarly to what gzlist said, I think here you should do
> >
> > try:
> > link = os.link
> > except AttributeError, ...
> > raise TransportNotPossible(...)
> >
> > link(...)
>
> Do you mean this:
>
> if osutils.hardlinks_good():
> def link(self, source, link_name):
> """See Transport.link."""
> try:
> link = os.link
> except AttributeError:
> return errors.TransportNotPossible("Hardlinks are not
> supported on %s" %self)
> except (IOError, OSError), e:
> self._translate_error(e, source)
>
> link(self._abspath(source), self._abspath(link_name))
>
> Or just straight up declaring a member named 'link' as an alias to os.link()?

Like that, except the osutils.hardlink_good() check seems redundant. Just declare the method always and let the AttributeError call handle the case that they're not supported.

Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

> However it seems like paramiko normally translates errors into
> exceptions itself, so you may not need to worry about this?

Not sure about that part. I think it raises errors on blatant errors (disconnection, non-support-- stuff that the library can't handle without human intervention, I suppose). For the most part, it returns error codes. Crude, but hey, I just work with it. :P

> Actually the thread seemed to conclude that it was better for Transport.stat()
> to call os.lstat, and then make the higher level code specifically deal with
> the symlink.

Okay, I've reverted that, then.

> Like that, except the osutils.hardlink_good() check seems redundant. Just
> declare the method always and let the AttributeError call handle the case that
> they're not supported.

Eh. Your suggestion conflicts with gz's. I do like the idea of letting the rest of bzrlib's guts tell me whether it's okay to declare a method, though, rather than handle an AttributeError myself. But you guys are more familiar with the gestalt of Bzr than I am, so perhaps I'm missing something?

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

On 3 March 2010 19:40, Neil Santos <email address hidden> wrote:
>> However it seems like paramiko normally translates errors into
>> exceptions itself, so you may not need to worry about this?
>
> Not sure about that part.  I think it raises errors on blatant errors (disconnection, non-support-- stuff that the library can't handle without human intervention, I suppose).  For the most part, it returns error codes.  Crude, but hey, I just work with it. :P

Heh, ok. So on the whole, I'd say if you get a non-ok return code,
just raise an error that includes that value; we could add a method
that specifically translates them if we think any are interesting.

>
>> Actually the thread seemed to conclude that it was better for Transport.stat()
>> to call os.lstat, and then make the higher level code specifically deal with
>> the symlink.
>
> Okay, I've reverted that, then.
>
>> Like that, except the osutils.hardlink_good() check seems redundant.  Just
>> declare the method always and let the AttributeError call handle the case that
>> they're not supported.
>
> Eh.  Your suggestion conflicts with gz's.  I do like the idea of letting the rest of bzrlib's guts tell me whether it's okay to declare a method, though, rather than handle an AttributeError myself.  But you guys are more familiar with the gestalt of Bzr than I am, so perhaps I'm missing something?

I think gz and I agree that there's no point in having redundant
checks. As he says, we have to handle the case that it turns out not
to be possible at runtime, and I guess it just seems a bit cleaner to
me to avoid conditionally declaring functions. Either is ok though,
so let me know when you're ready for this to be rereviewed/merged.

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

Revision history for this message
Martin Packman (gz) wrote : Posted in a previous version of this proposal

> > Eh.  Your suggestion conflicts with gz's.  I do like the idea of letting the
> rest of bzrlib's guts tell me whether it's okay to declare a method, though,
> rather than handle an AttributeError myself.  But you guys are more familiar
> with the gestalt of Bzr than I am, so perhaps I'm missing something?
>
> I think gz and I agree that there's no point in having redundant
> checks. As he says, we have to handle the case that it turns out not
> to be possible at runtime, and I guess it just seems a bit cleaner to
> me to avoid conditionally declaring functions. Either is ok though,
> so let me know when you're ready for this to be rereviewed/merged.

To be clear, I was trying to say the AttributeError handling is better written outside the method, but the functions still may throw EnvironmentError when called so that part had to be retained. It's a minor style point however.

Revision history for this message
Neil Santos (rls) wrote : Posted in a previous version of this proposal

Resubmitting for review.

review: Needs Resubmitting
Revision history for this message
Martin Packman (gz) wrote :

Okay, so doing it my way means you don't need to catch AttributeError any more, pull <lp:~gz/bzr/transport-link-and-symlink-support> to update your branch.

Tested the changes locally and they seem find.

review: Approve
Revision history for this message
Neil Santos (rls) wrote :

> Tested the changes locally and they seem find.

Hunh. Paramiko didn't throw 'Operation unsupported' for you for the SFTPTransport symlink() test? It does for me, which was one of the reasons why I put this branch up for review earlier than I though it deserved. :P

Revision history for this message
Martin Packman (gz) wrote :

None of my boxes have paramiko installed.

I presume you could change test_symlink to turn whatever paramiko throws into a KnownFailure for the moment.

While looking at that, seems to have this comment twice, second should be 'symlinks':

+ # This transport doesn't do hardlinks

Revision history for this message
Martin Pool (mbp) wrote :

As I mentioned before, I'd probably s/link/hardlink/ for the sake of obviousness. I'll see about integrating this.

review: Approve
Revision history for this message
Neil Santos (rls) wrote :

> As I mentioned before, I'd probably s/link/hardlink/ for the sake of
> obviousness. I'll see about integrating this.

Done.

On an unrelated note, should I do an 'Abstain' review or something? Launchpad seems to be expecting one from me.

Revision history for this message
Neil Santos (rls) wrote :

Nothing more to add at this point. (And LP seems to be waiting for me to do something.)

review: Approve
Revision history for this message
Martin Pool (mbp) wrote :

good to go, I'll send it in

It probably should get news about the api change. I'll add that.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/tests/per_transport.py'
2--- bzrlib/tests/per_transport.py 2010-02-23 07:43:11 +0000
3+++ bzrlib/tests/per_transport.py 2010-03-05 05:34:24 +0000
4@@ -1083,6 +1083,52 @@
5 subdir.stat('./file')
6 subdir.stat('.')
7
8+ def test_hardlink(self):
9+ from stat import ST_NLINK
10+
11+ t = self.get_transport()
12+
13+ source_name = "original_target"
14+ link_name = "target_link"
15+
16+ self.build_tree([source_name], transport=t)
17+
18+ try:
19+ t.hardlink(source_name, link_name)
20+
21+ self.failUnless(t.has(source_name))
22+ self.failUnless(t.has(link_name))
23+
24+ st = t.stat(link_name)
25+ self.failUnlessEqual(st[ST_NLINK], 2)
26+ except TransportNotPossible:
27+ raise TestSkipped("Transport %s does not support hardlinks." %
28+ self._server.__class__)
29+
30+ def test_symlink(self):
31+ from stat import S_ISLNK
32+
33+ t = self.get_transport()
34+
35+ source_name = "original_target"
36+ link_name = "target_link"
37+
38+ self.build_tree([source_name], transport=t)
39+
40+ try:
41+ t.symlink(source_name, link_name)
42+
43+ self.failUnless(t.has(source_name))
44+ self.failUnless(t.has(link_name))
45+
46+ st = t.stat(link_name)
47+ self.failUnless(S_ISLNK(st.st_mode))
48+ except TransportNotPossible:
49+ raise TestSkipped("Transport %s does not support symlinks." %
50+ self._server.__class__)
51+ except IOError:
52+ raise tests.KnownFailure("Paramiko fails to create symlinks during tests")
53+
54 def test_list_dir(self):
55 # TODO: Test list_dir, just try once, and if it throws, stop testing
56 t = self.get_transport()
57
58=== modified file 'bzrlib/transport/__init__.py'
59--- bzrlib/transport/__init__.py 2010-02-23 07:43:11 +0000
60+++ bzrlib/transport/__init__.py 2010-03-05 05:34:24 +0000
61@@ -1202,6 +1202,18 @@
62 count = self._iterate_over(relpaths, gather, pb, 'stat', expand=False)
63 return stats
64
65+ def readlink(self, relpath):
66+ """Return a string representing the path to which the symbolic link points."""
67+ raise errors.TransportNotPossible("Dereferencing symlinks is not supported on %s" % self)
68+
69+ def hardlink(self, source, link_name):
70+ """Create a hardlink pointing to source named link_name."""
71+ raise errors.TransportNotPossible("Hard links are not supported on %s" % self)
72+
73+ def symlink(self, source, link_name):
74+ """Create a symlink pointing to source named link_name."""
75+ raise errors.TransportNotPossible("Symlinks are not supported on %s" % self)
76+
77 def listable(self):
78 """Return True if this store supports listing."""
79 raise NotImplementedError(self.listable)
80@@ -1831,6 +1843,6 @@
81
82
83 transport_server_registry = registry.Registry()
84-transport_server_registry.register_lazy('bzr', 'bzrlib.smart.server',
85+transport_server_registry.register_lazy('bzr', 'bzrlib.smart.server',
86 'serve_bzr', help="The Bazaar smart server protocol over TCP. (default port: 4155)")
87 transport_server_registry.default_key = 'bzr'
88
89=== modified file 'bzrlib/transport/local.py'
90--- bzrlib/transport/local.py 2010-02-23 07:43:11 +0000
91+++ bzrlib/transport/local.py 2010-03-05 05:34:24 +0000
92@@ -481,7 +481,7 @@
93 path = relpath
94 try:
95 path = self._abspath(relpath)
96- return os.stat(path)
97+ return os.lstat(path)
98 except (IOError, OSError),e:
99 self._translate_error(e, path)
100
101@@ -515,6 +515,33 @@
102 except (IOError, OSError),e:
103 self._translate_error(e, path)
104
105+ if osutils.host_os_dereferences_symlinks():
106+ def readlink(self, relpath):
107+ """See Transport.readlink."""
108+ return osutils.readlink(self._abspath(relpath))
109+
110+ if osutils.hardlinks_good():
111+ def hardlink(self, source, link_name):
112+ """See Transport.link."""
113+ try:
114+ os.link(self._abspath(source), self._abspath(link_name))
115+ except (IOError, OSError), e:
116+ self._translate_error(e, source)
117+
118+ if osutils.has_symlinks():
119+ def symlink(self, source, link_name):
120+ """See Transport.symlink."""
121+ abs_link_dirpath = urlutils.dirname(self.abspath(link_name))
122+ source_rel = urlutils.file_relpath(
123+ urlutils.strip_trailing_slash(abs_link_dirpath),
124+ urlutils.strip_trailing_slash(self.abspath(source))
125+ )
126+
127+ try:
128+ os.symlink(source_rel, self._abspath(link_name))
129+ except (IOError, OSError), e:
130+ self._translate_error(e, source_rel)
131+
132 def _can_roundtrip_unix_modebits(self):
133 if sys.platform == 'win32':
134 # anyone else?
135
136=== modified file 'bzrlib/transport/sftp.py'
137--- bzrlib/transport/sftp.py 2010-02-23 07:43:11 +0000
138+++ bzrlib/transport/sftp.py 2010-03-05 05:34:24 +0000
139@@ -82,7 +82,7 @@
140 else:
141 from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
142 SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
143- CMD_HANDLE, CMD_OPEN)
144+ SFTP_OK, CMD_HANDLE, CMD_OPEN)
145 from paramiko.sftp_attr import SFTPAttributes
146 from paramiko.sftp_file import SFTPFile
147
148@@ -810,10 +810,32 @@
149 """Return the stat information for a file."""
150 path = self._remote_path(relpath)
151 try:
152- return self._get_sftp().stat(path)
153+ return self._get_sftp().lstat(path)
154 except (IOError, paramiko.SSHException), e:
155 self._translate_io_exception(e, path, ': unable to stat')
156
157+ def readlink(self, relpath):
158+ """See Transport.readlink."""
159+ path = self._remote_path(relpath)
160+ try:
161+ return self._get_sftp().readlink(path)
162+ except (IOError, paramiko.SSHException), e:
163+ self._translate_io_exception(e, path, ': unable to readlink')
164+
165+ def symlink(self, source, link_name):
166+ """See Transport.symlink."""
167+ try:
168+ conn = self._get_sftp()
169+ sftp_retval = conn.symlink(source, link_name)
170+ if SFTP_OK != sftp_retval:
171+ raise TransportError(
172+ '%r: unable to create symlink to %r' % (link_name, source),
173+ sftp_retval
174+ )
175+ except (IOError, paramiko.SSHException), e:
176+ self._translate_io_exception(e, link_name,
177+ ': unable to create symlink to %r' % (source))
178+
179 def lock_read(self, relpath):
180 """
181 Lock the given file for shared (read) access.