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

Proposed by Neil Santos
Status: Superseded
Proposed branch: lp:~rls/bzr/transport-link-and-symlink-support
Merge into: lp:bzr
Diff against target: 185 lines (+115/-4)
4 files modified
bzrlib/tests/per_transport.py (+44/-0)
bzrlib/transport/__init__.py (+13/-1)
bzrlib/transport/local.py (+34/-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
Neil Santos (community) Needs Resubmitting
Martin Pool Needs Fixing
Martin Packman (community) Needs Fixing
Review via email: mp+20096@code.launchpad.net

This proposal has been superseded by a proposal from 2010-03-04.

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

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 :

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 :

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 :

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 :

> 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 :
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 :

> 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 :

> + 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 :

> > 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 :

> 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 :

> > 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 :

> 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 :

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 :

> > 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 :

Resubmitting for review.

review: Needs Resubmitting

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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-03-04 03:26:22 +0000
@@ -1083,6 +1083,50 @@
1083 subdir.stat('./file')1083 subdir.stat('./file')
1084 subdir.stat('.')1084 subdir.stat('.')
10851085
1086 def test_link(self):
1087 from stat import ST_NLINK
1088
1089 t = self.get_transport()
1090
1091 source_name = "original_target"
1092 link_name = "target_link"
1093
1094 self.build_tree([source_name], transport=t)
1095
1096 try:
1097 t.link(source_name, link_name)
1098
1099 self.failUnless(t.has(source_name))
1100 self.failUnless(t.has(link_name))
1101
1102 st = t.stat(link_name)
1103 self.failUnlessEqual(st[ST_NLINK], 2)
1104 except TransportNotPossible:
1105 # This transport doesn't do hardlinks
1106 return
1107
1108 def test_symlink(self):
1109 from stat import S_ISLNK
1110
1111 t = self.get_transport()
1112
1113 source_name = "original_target"
1114 link_name = "target_link"
1115
1116 self.build_tree([source_name], transport=t)
1117
1118 try:
1119 t.symlink(source_name, link_name)
1120
1121 self.failUnless(t.has(source_name))
1122 self.failUnless(t.has(link_name))
1123
1124 st = t.stat(link_name)
1125 self.failUnless(S_ISLNK(st.st_mode))
1126 except TransportNotPossible:
1127 # This transport doesn't do hardlinks
1128 return
1129
1086 def test_list_dir(self):1130 def test_list_dir(self):
1087 # TODO: Test list_dir, just try once, and if it throws, stop testing1131 # TODO: Test list_dir, just try once, and if it throws, stop testing
1088 t = self.get_transport()1132 t = self.get_transport()
10891133
=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py 2010-02-23 07:43:11 +0000
+++ bzrlib/transport/__init__.py 2010-03-04 03:26:22 +0000
@@ -1202,6 +1202,18 @@
1202 count = self._iterate_over(relpaths, gather, pb, 'stat', expand=False)1202 count = self._iterate_over(relpaths, gather, pb, 'stat', expand=False)
1203 return stats1203 return stats
12041204
1205 def readlink(self, relpath):
1206 """Return a string representing the path to which the symbolic link points."""
1207 raise errors.TransportNotPossible("Dereferencing symlinks is not supported on %s" % self)
1208
1209 def link(self, source, link_name):
1210 """Create a hardlink pointing to source named link_name."""
1211 raise errors.TransportNotPossible("Hard links are not supported on %s" % self)
1212
1213 def symlink(self, source, link_name):
1214 """Create a symlink pointing to source named link_name."""
1215 raise errors.TransportNotPossible("Symlinks are not supported on %s" % self)
1216
1205 def listable(self):1217 def listable(self):
1206 """Return True if this store supports listing."""1218 """Return True if this store supports listing."""
1207 raise NotImplementedError(self.listable)1219 raise NotImplementedError(self.listable)
@@ -1831,6 +1843,6 @@
18311843
18321844
1833transport_server_registry = registry.Registry()1845transport_server_registry = registry.Registry()
1834transport_server_registry.register_lazy('bzr', 'bzrlib.smart.server', 1846transport_server_registry.register_lazy('bzr', 'bzrlib.smart.server',
1835 'serve_bzr', help="The Bazaar smart server protocol over TCP. (default port: 4155)")1847 'serve_bzr', help="The Bazaar smart server protocol over TCP. (default port: 4155)")
1836transport_server_registry.default_key = 'bzr'1848transport_server_registry.default_key = 'bzr'
18371849
=== modified file 'bzrlib/transport/local.py'
--- bzrlib/transport/local.py 2010-02-23 07:43:11 +0000
+++ bzrlib/transport/local.py 2010-03-04 03:26:22 +0000
@@ -481,7 +481,7 @@
481 path = relpath481 path = relpath
482 try:482 try:
483 path = self._abspath(relpath)483 path = self._abspath(relpath)
484 return os.stat(path)484 return os.lstat(path)
485 except (IOError, OSError),e:485 except (IOError, OSError),e:
486 self._translate_error(e, path)486 self._translate_error(e, path)
487487
@@ -515,6 +515,39 @@
515 except (IOError, OSError),e:515 except (IOError, OSError),e:
516 self._translate_error(e, path)516 self._translate_error(e, path)
517517
518 if osutils.host_os_dereferences_symlinks():
519 def readlink(self, relpath):
520 """See Transport.readlink."""
521 return osutils.readlink(self._abspath(relpath))
522
523 if osutils.hardlinks_good():
524 def link(self, source, link_name):
525 """See Transport.link."""
526 try:
527 link = os.link
528 except AttributeError:
529 return errors.TransportNotPossible("Hardlinks are not supported on %s" %self)
530 except (IOError, OSError), e:
531 self._translate_error(e, source)
532
533 link(self._abspath(source), self._abspath(link_name))
534
535 if osutils.has_symlinks():
536 def symlink(self, source, link_name):
537 """See Transport.symlink."""
538 abs_link_dirpath = urlutils.dirname(self.abspath(link_name))
539 source_rel = urlutils.file_relpath(
540 urlutils.strip_trailing_slash(abs_link_dirpath),
541 urlutils.strip_trailing_slash(self.abspath(source))
542 )
543
544 try:
545 os.symlink(source_rel, self._abspath(link_name))
546 except AttributeError:
547 raise errors.TransportNotPossible("Symlinks are not supported on %s" % self)
548 except (IOError, OSError), e:
549 self._translate_error(e, source_rel)
550
518 def _can_roundtrip_unix_modebits(self):551 def _can_roundtrip_unix_modebits(self):
519 if sys.platform == 'win32':552 if sys.platform == 'win32':
520 # anyone else?553 # anyone else?
521554
=== modified file 'bzrlib/transport/sftp.py'
--- bzrlib/transport/sftp.py 2010-02-23 07:43:11 +0000
+++ bzrlib/transport/sftp.py 2010-03-04 03:26:22 +0000
@@ -82,7 +82,7 @@
82else:82else:
83 from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,83 from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
84 SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,84 SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
85 CMD_HANDLE, CMD_OPEN)85 SFTP_OK, CMD_HANDLE, CMD_OPEN)
86 from paramiko.sftp_attr import SFTPAttributes86 from paramiko.sftp_attr import SFTPAttributes
87 from paramiko.sftp_file import SFTPFile87 from paramiko.sftp_file import SFTPFile
8888
@@ -810,10 +810,32 @@
810 """Return the stat information for a file."""810 """Return the stat information for a file."""
811 path = self._remote_path(relpath)811 path = self._remote_path(relpath)
812 try:812 try:
813 return self._get_sftp().stat(path)813 return self._get_sftp().lstat(path)
814 except (IOError, paramiko.SSHException), e:814 except (IOError, paramiko.SSHException), e:
815 self._translate_io_exception(e, path, ': unable to stat')815 self._translate_io_exception(e, path, ': unable to stat')
816816
817 def readlink(self, relpath):
818 """See Transport.readlink."""
819 path = self._remote_path(relpath)
820 try:
821 return self._get_sftp().readlink(path)
822 except (IOError, paramiko.SSHException), e:
823 self._translate_io_exception(e, path, ': unable to readlink')
824
825 def symlink(self, source, link_name):
826 """See Transport.symlink."""
827 try:
828 conn = self._get_sftp()
829 sftp_retval = conn.symlink(source, link_name)
830 if SFTP_OK != sftp_retval:
831 raise TransportError(
832 '%r: unable to create symlink to %r' % (link_name, source),
833 sftp_retval
834 )
835 except (IOError, paramiko.SSHException), e:
836 self._translate_io_exception(e, link_name,
837 ': unable to create symlink to %r' % (source))
838
817 def lock_read(self, relpath):839 def lock_read(self, relpath):
818 """840 """
819 Lock the given file for shared (read) access.841 Lock the given file for shared (read) access.