add SFTPClient.truncate.  add chmod, chown, utime, and truncate to SFTPFile.  and of course tests.
This commit is contained in:
Robey Pointer 2006-02-19 19:17:41 -08:00
parent 8446c0bda1
commit 4be91d1c4d
5 changed files with 193 additions and 68 deletions

View File

@ -59,7 +59,7 @@ class SFTPClient (BaseSFTP):
An alternate way to create an SFTP client context is by using An alternate way to create an SFTP client context is by using
L{from_transport}. L{from_transport}.
@param sock: an open L{Channel} using the C{"sftp"} subsystem. @param sock: an open L{Channel} using the C{"sftp"} subsystem
@type sock: L{Channel} @type sock: L{Channel}
""" """
BaseSFTP.__init__(self) BaseSFTP.__init__(self)
@ -84,10 +84,10 @@ class SFTPClient (BaseSFTP):
""" """
Create an SFTP client channel from an open L{Transport}. Create an SFTP client channel from an open L{Transport}.
@param t: an open L{Transport} which is already authenticated. @param t: an open L{Transport} which is already authenticated
@type t: L{Transport} @type t: L{Transport}
@return: a new L{SFTPClient} object, referring to an sftp session @return: a new L{SFTPClient} object, referring to an sftp session
(channel) across the transport. (channel) across the transport
@rtype: L{SFTPClient} @rtype: L{SFTPClient}
""" """
chan = t.open_session() chan = t.open_session()
@ -185,13 +185,13 @@ class SFTPClient (BaseSFTP):
buffering, C{1} uses line buffering, and any number greater than 1 buffering, C{1} uses line buffering, and any number greater than 1
(C{>1}) uses that specific buffer size. (C{>1}) uses that specific buffer size.
@param filename: name of the file to open. @param filename: name of the file to open
@type filename: string @type filename: str
@param mode: mode (python-style) to open in. @param mode: mode (python-style) to open in
@type mode: string @type mode: str
@param bufsize: desired buffering (-1 = default buffer size) @param bufsize: desired buffering (-1 = default buffer size)
@type bufsize: int @type bufsize: int
@return: a file object representing the open file. @return: a file object representing the open file
@rtype: SFTPFile @rtype: SFTPFile
@raise IOError: if the file could not be opened. @raise IOError: if the file could not be opened.
@ -223,13 +223,13 @@ class SFTPClient (BaseSFTP):
def remove(self, path): def remove(self, path):
""" """
Remove the file at the given path. Remove the file at the given path. This only works on files; for
removing folders (directories), use L{rmdir}.
@param path: path (absolute or relative) of the file to remove. @param path: path (absolute or relative) of the file to remove
@type path: string @type path: str
@raise IOError: if the path refers to a folder (directory). Use @raise IOError: if the path refers to a folder (directory)
L{rmdir} to remove a folder.
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'remove(%r)' % path) self._log(DEBUG, 'remove(%r)' % path)
@ -241,13 +241,13 @@ class SFTPClient (BaseSFTP):
""" """
Rename a file or folder from C{oldpath} to C{newpath}. Rename a file or folder from C{oldpath} to C{newpath}.
@param oldpath: existing name of the file or folder. @param oldpath: existing name of the file or folder
@type oldpath: string @type oldpath: str
@param newpath: new name for the file or folder. @param newpath: new name for the file or folder
@type newpath: string @type newpath: str
@raise IOError: if C{newpath} is a folder, or something else goes @raise IOError: if C{newpath} is a folder, or something else goes
wrong. wrong
""" """
oldpath = self._adjust_cwd(oldpath) oldpath = self._adjust_cwd(oldpath)
newpath = self._adjust_cwd(newpath) newpath = self._adjust_cwd(newpath)
@ -260,9 +260,9 @@ class SFTPClient (BaseSFTP):
The default mode is 0777 (octal). On some systems, mode is ignored. The default mode is 0777 (octal). On some systems, mode is ignored.
Where it is used, the current umask value is first masked out. Where it is used, the current umask value is first masked out.
@param path: name of the folder to create. @param path: name of the folder to create
@type path: string @type path: str
@param mode: permissions (posix-style) for the newly-created folder. @param mode: permissions (posix-style) for the newly-created folder
@type mode: int @type mode: int
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
@ -275,8 +275,8 @@ class SFTPClient (BaseSFTP):
""" """
Remove the folder named C{path}. Remove the folder named C{path}.
@param path: name of the folder to remove. @param path: name of the folder to remove
@type path: string @type path: str
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
self._log(DEBUG, 'rmdir(%r)' % path) self._log(DEBUG, 'rmdir(%r)' % path)
@ -296,9 +296,9 @@ class SFTPClient (BaseSFTP):
The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid}, The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid},
C{st_atime}, and C{st_mtime}. C{st_atime}, and C{st_mtime}.
@param path: the filename to stat. @param path: the filename to stat
@type path: string @type path: str
@return: an object containing attributes about the given file. @return: an object containing attributes about the given file
@rtype: SFTPAttributes @rtype: SFTPAttributes
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
@ -314,9 +314,9 @@ class SFTPClient (BaseSFTP):
following symbolic links (shortcuts). This otherwise behaves exactly following symbolic links (shortcuts). This otherwise behaves exactly
the same as L{stat}. the same as L{stat}.
@param path: the filename to stat. @param path: the filename to stat
@type path: string @type path: str
@return: an object containing attributes about the given file. @return: an object containing attributes about the given file
@rtype: SFTPAttributes @rtype: SFTPAttributes
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
@ -331,10 +331,10 @@ class SFTPClient (BaseSFTP):
Create a symbolic link (shortcut) of the C{source} path at Create a symbolic link (shortcut) of the C{source} path at
C{destination}. C{destination}.
@param source: path of the original file. @param source: path of the original file
@type source: string @type source: str
@param dest: path of the newly created symlink. @param dest: path of the newly created symlink
@type dest: string @type dest: str
""" """
dest = self._adjust_cwd(dest) dest = self._adjust_cwd(dest)
self._log(DEBUG, 'symlink(%r, %r)' % (source, dest)) self._log(DEBUG, 'symlink(%r, %r)' % (source, dest))
@ -348,9 +348,9 @@ class SFTPClient (BaseSFTP):
unix-style and identical to those used by python's C{os.chmod} unix-style and identical to those used by python's C{os.chmod}
function. function.
@param path: path of the file to change the permissions of. @param path: path of the file to change the permissions of
@type path: string @type path: str
@param mode: new permissions. @param mode: new permissions
@type mode: int @type mode: int
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
@ -366,8 +366,8 @@ class SFTPClient (BaseSFTP):
only want to change one, use L{stat} first to retrieve the current only want to change one, use L{stat} first to retrieve the current
owner and group. owner and group.
@param path: path of the file to change the owner and group of. @param path: path of the file to change the owner and group of
@type path: string @type path: str
@param uid: new owner's uid @param uid: new owner's uid
@type uid: int @type uid: int
@param gid: new group id @param gid: new group id
@ -388,11 +388,11 @@ class SFTPClient (BaseSFTP):
modified times, respectively. This bizarre API is mimicked from python modified times, respectively. This bizarre API is mimicked from python
for the sake of consistency -- I apologize. for the sake of consistency -- I apologize.
@param path: path of the file to modify. @param path: path of the file to modify
@type path: string @type path: str
@param times: C{None} or a tuple of (access time, modified time) in @param times: C{None} or a tuple of (access time, modified time) in
standard internet epoch time (seconds since 01 January 1970 GMT). standard internet epoch time (seconds since 01 January 1970 GMT)
@type times: tuple of int @type times: tuple(int)
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
if times is None: if times is None:
@ -402,15 +402,32 @@ class SFTPClient (BaseSFTP):
attr.st_atime, attr.st_mtime = times attr.st_atime, attr.st_mtime = times
self._request(CMD_SETSTAT, path, attr) self._request(CMD_SETSTAT, path, attr)
def truncate(self, path, size):
"""
Change the size of the file specified by C{path}. This usually extends
or shrinks the size of the file, just like the C{truncate()} method on
python file objects.
@param path: path of the file to modify
@type path: str
@param size: the new size of the file
@type size: int or long
"""
path = self._adjust_cwd(path)
self._log(DEBUG, 'truncate(%r, %r)' % (path, size))
attr = SFTPAttributes()
attr.st_size = size
self._request(CMD_SETSTAT, path, attr)
def readlink(self, path): def readlink(self, path):
""" """
Return the target of a symbolic link (shortcut). You can use Return the target of a symbolic link (shortcut). You can use
L{symlink} to create these. The result may be either an absolute or L{symlink} to create these. The result may be either an absolute or
relative pathname. relative pathname.
@param path: path of the symbolic link file. @param path: path of the symbolic link file
@type path: str @type path: str
@return: target path. @return: target path
@rtype: str @rtype: str
""" """
path = self._adjust_cwd(path) path = self._adjust_cwd(path)
@ -432,9 +449,9 @@ class SFTPClient (BaseSFTP):
server is considering to be the "current folder" (by passing C{'.'} server is considering to be the "current folder" (by passing C{'.'}
as C{path}). as C{path}).
@param path: path to be normalized. @param path: path to be normalized
@type path: str @type path: str
@return: normalized form of the given path. @return: normalized form of the given path
@rtype: str @rtype: str
@raise IOError: if the path can't be resolved on the server @raise IOError: if the path can't be resolved on the server

View File

@ -181,6 +181,71 @@ class SFTPFile (BufferedFile):
if t != CMD_ATTRS: if t != CMD_ATTRS:
raise SFTPError('Expected attributes') raise SFTPError('Expected attributes')
return SFTPAttributes._from_msg(msg) return SFTPAttributes._from_msg(msg)
def chmod(self, mode):
"""
Change the mode (permissions) of this file. The permissions are
unix-style and identical to those used by python's C{os.chmod}
function.
@param mode: new permissions
@type mode: int
"""
self.sftp._log(DEBUG, 'chmod(%s, %r)' % (util.hexify(self.handle), mode))
attr = SFTPAttributes()
attr.st_mode = mode
self.sftp._request(CMD_FSETSTAT, self.handle, attr)
def chown(self, uid, gid):
"""
Change the owner (C{uid}) and group (C{gid}) of this file. As with
python's C{os.chown} function, you must pass both arguments, so if you
only want to change one, use L{stat} first to retrieve the current
owner and group.
@param uid: new owner's uid
@type uid: int
@param gid: new group id
@type gid: int
"""
self.sftp._log(DEBUG, 'chown(%s, %r, %r)' % (util.hexify(self.handle), uid, gid))
attr = SFTPAttributes()
attr.st_uid, attr.st_gid = uid, gid
self.sftp._request(CMD_FSETSTAT, self.handle, attr)
def utime(self, times):
"""
Set the access and modified times of this file. If
C{times} is C{None}, then the file's access and modified times are set
to the current time. Otherwise, C{times} must be a 2-tuple of numbers,
of the form C{(atime, mtime)}, which is used to set the access and
modified times, respectively. This bizarre API is mimicked from python
for the sake of consistency -- I apologize.
@param times: C{None} or a tuple of (access time, modified time) in
standard internet epoch time (seconds since 01 January 1970 GMT)
@type times: tuple(int)
"""
if times is None:
times = (time.time(), time.time())
self.sftp._log(DEBUG, 'utime(%s, %r)' % (util.hexify(self.handle), times))
attr = SFTPAttributes()
attr.st_atime, attr.st_mtime = times
self.sftp._request(CMD_FSETSTAT, self.handle, attr)
def truncate(self, size):
"""
Change the size of this file. This usually extends
or shrinks the size of the file, just like the C{truncate()} method on
python file objects.
@param size: the new size of the file
@type size: int or long
"""
self.sftp._log(DEBUG, 'truncate(%s, %r)' % (util.hexify(self.handle), size))
attr = SFTPAttributes()
attr.st_size = size
self.sftp._request(CMD_FSETSTAT, self.handle, attr)
def check(self, hash_algorithm, offset=0, length=0, block_size=0): def check(self, hash_algorithm, offset=0, length=0, block_size=0):
""" """

View File

@ -147,6 +147,8 @@ class SFTPServer (BaseSFTP, SubsystemHandler):
os.chown(filename, attr.st_uid, attr.st_gid) os.chown(filename, attr.st_uid, attr.st_gid)
if attr._flags & attr.FLAG_AMTIME: if attr._flags & attr.FLAG_AMTIME:
os.utime(filename, (attr.st_atime, attr.st_mtime)) os.utime(filename, (attr.st_atime, attr.st_mtime))
if attr._flags & attr.FLAG_SIZE:
open(filename, 'w+').truncate(attr.st_size)
set_file_attr = staticmethod(set_file_attr) set_file_attr = staticmethod(set_file_attr)

View File

@ -48,6 +48,7 @@ class StubSFTPHandle (SFTPHandle):
# use the stored filename # use the stored filename
try: try:
SFTPServer.set_file_attr(self.filename, attr) SFTPServer.set_file_attr(self.filename, attr)
return SFTP_OK
except OSError, e: except OSError, e:
return SFTPServer.convert_errno(e.errno) return SFTPServer.convert_errno(e.errno)

View File

@ -273,28 +273,68 @@ class SFTPTest (unittest.TestCase):
def test_8_setstat(self): def test_8_setstat(self):
""" """
verify that the setstat functions (chown, chmod, utime) work. verify that the setstat functions (chown, chmod, utime, truncate) work.
""" """
f = sftp.open(FOLDER + '/special', 'w') f = sftp.open(FOLDER + '/special', 'w')
try: try:
f.write('x' * 1024)
f.close() f.close()
stat = sftp.stat(FOLDER + '/special') stat = sftp.stat(FOLDER + '/special')
sftp.chmod(FOLDER + '/special', (stat.st_mode & ~0777) | 0600) sftp.chmod(FOLDER + '/special', (stat.st_mode & ~0777) | 0600)
self.assertEqual(sftp.stat(FOLDER + '/special').st_mode & 0777, 0600) stat = sftp.stat(FOLDER + '/special')
self.assertEqual(stat.st_mode & 0777, 0600)
self.assertEqual(stat.st_size, 1024)
mtime = stat.st_mtime - 3600 mtime = stat.st_mtime - 3600
atime = stat.st_atime - 1800 atime = stat.st_atime - 1800
sftp.utime(FOLDER + '/special', (atime, mtime)) sftp.utime(FOLDER + '/special', (atime, mtime))
nstat = sftp.stat(FOLDER + '/special') stat = sftp.stat(FOLDER + '/special')
self.assertEqual(nstat.st_mtime, mtime) self.assertEqual(stat.st_mtime, mtime)
self.assertEqual(nstat.st_atime, atime) self.assertEqual(stat.st_atime, atime)
# can't really test chown, since we'd have to know a valid uid. # can't really test chown, since we'd have to know a valid uid.
sftp.truncate(FOLDER + '/special', 512)
stat = sftp.stat(FOLDER + '/special')
self.assertEqual(stat.st_size, 512)
finally: finally:
sftp.remove(FOLDER + '/special') sftp.remove(FOLDER + '/special')
def test_9_readline_seek(self): def test_9_fsetstat(self):
"""
verify that the fsetstat functions (chown, chmod, utime, truncate)
work on open files.
"""
f = sftp.open(FOLDER + '/special', 'w')
try:
f.write('x' * 1024)
f.close()
f = sftp.open(FOLDER + '/special', 'r+')
stat = f.stat()
f.chmod((stat.st_mode & ~0777) | 0600)
stat = f.stat()
self.assertEqual(stat.st_mode & 0777, 0600)
self.assertEqual(stat.st_size, 1024)
mtime = stat.st_mtime - 3600
atime = stat.st_atime - 1800
f.utime((atime, mtime))
stat = f.stat()
self.assertEqual(stat.st_mtime, mtime)
self.assertEqual(stat.st_atime, atime)
# can't really test chown, since we'd have to know a valid uid.
f.truncate(512)
stat = f.stat()
self.assertEqual(stat.st_size, 512)
f.close()
finally:
sftp.remove(FOLDER + '/special')
def test_A_readline_seek(self):
""" """
create a text file and write a bunch of text into it. then count the lines create a text file and write a bunch of text into it. then count the lines
in the file, and seek around to retreive particular lines. this should in the file, and seek around to retreive particular lines. this should
@ -324,7 +364,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove(FOLDER + '/duck.txt') sftp.remove(FOLDER + '/duck.txt')
def test_A_write_seek(self): def test_B_write_seek(self):
""" """
create a text file, seek back and change part of it, and verify that the create a text file, seek back and change part of it, and verify that the
changes worked. changes worked.
@ -344,7 +384,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove(FOLDER + '/testing.txt') sftp.remove(FOLDER + '/testing.txt')
def test_B_symlink(self): def test_C_symlink(self):
""" """
create a symlink and then check that lstat doesn't follow it. create a symlink and then check that lstat doesn't follow it.
""" """
@ -387,7 +427,7 @@ class SFTPTest (unittest.TestCase):
except: except:
pass pass
def test_C_flush_seek(self): def test_D_flush_seek(self):
""" """
verify that buffered writes are automatically flushed on seek. verify that buffered writes are automatically flushed on seek.
""" """
@ -409,7 +449,7 @@ class SFTPTest (unittest.TestCase):
except: except:
pass pass
def test_D_lots_of_files(self): def test_E_lots_of_files(self):
""" """
create a bunch of files over the same session. create a bunch of files over the same session.
""" """
@ -440,7 +480,7 @@ class SFTPTest (unittest.TestCase):
except: except:
pass pass
def test_E_big_file(self): def test_F_big_file(self):
""" """
write a 1MB file with no buffering. write a 1MB file with no buffering.
""" """
@ -474,7 +514,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove('%s/hongry.txt' % FOLDER) sftp.remove('%s/hongry.txt' % FOLDER)
def test_F_big_file_pipelined(self): def test_G_big_file_pipelined(self):
""" """
write a 1MB file, with no linefeeds, using pipelining. write a 1MB file, with no linefeeds, using pipelining.
""" """
@ -510,7 +550,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove('%s/hongry.txt' % FOLDER) sftp.remove('%s/hongry.txt' % FOLDER)
def test_G_lots_of_prefetching(self): def test_H_lots_of_prefetching(self):
""" """
prefetch a 1MB file a bunch of times, discarding the file object prefetch a 1MB file a bunch of times, discarding the file object
without using it, to verify that paramiko doesn't get confused. without using it, to verify that paramiko doesn't get confused.
@ -546,7 +586,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove('%s/hongry.txt' % FOLDER) sftp.remove('%s/hongry.txt' % FOLDER)
def test_H_big_file_big_buffer(self): def test_I_big_file_big_buffer(self):
""" """
write a 1MB file, with no linefeeds, and a big buffer. write a 1MB file, with no linefeeds, and a big buffer.
""" """
@ -563,7 +603,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.remove('%s/hongry.txt' % FOLDER) sftp.remove('%s/hongry.txt' % FOLDER)
def test_I_big_file_renegotiate(self): def test_J_big_file_renegotiate(self):
""" """
write a 1MB file, forcing key renegotiation in the middle. write a 1MB file, forcing key renegotiation in the middle.
""" """
@ -585,7 +625,7 @@ class SFTPTest (unittest.TestCase):
sftp.remove('%s/hongry.txt' % FOLDER) sftp.remove('%s/hongry.txt' % FOLDER)
t.packetizer.REKEY_BYTES = pow(2, 30) t.packetizer.REKEY_BYTES = pow(2, 30)
def test_J_realpath(self): def test_K_realpath(self):
""" """
test that realpath is returning something non-empty and not an test that realpath is returning something non-empty and not an
error. error.
@ -596,7 +636,7 @@ class SFTPTest (unittest.TestCase):
self.assert_(len(f) > 0) self.assert_(len(f) > 0)
self.assertEquals(os.path.join(pwd, FOLDER), f) self.assertEquals(os.path.join(pwd, FOLDER), f)
def test_K_mkdir(self): def test_L_mkdir(self):
""" """
verify that mkdir/rmdir work. verify that mkdir/rmdir work.
""" """
@ -619,7 +659,7 @@ class SFTPTest (unittest.TestCase):
except IOError: except IOError:
pass pass
def test_L_chdir(self): def test_M_chdir(self):
""" """
verify that chdir/getcwd work. verify that chdir/getcwd work.
""" """
@ -656,7 +696,7 @@ class SFTPTest (unittest.TestCase):
except: except:
pass pass
def test_M_get_put(self): def test_N_get_put(self):
""" """
verify that get/put work. verify that get/put work.
""" """
@ -685,7 +725,7 @@ class SFTPTest (unittest.TestCase):
os.unlink(localname) os.unlink(localname)
sftp.unlink(FOLDER + '/bunny.txt') sftp.unlink(FOLDER + '/bunny.txt')
def test_N_check(self): def test_O_check(self):
""" """
verify that file.check() works against our own server. verify that file.check() works against our own server.
(it's an sftp extension that we support, and may be the only ones who (it's an sftp extension that we support, and may be the only ones who
@ -707,7 +747,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.unlink(FOLDER + '/kitty.txt') sftp.unlink(FOLDER + '/kitty.txt')
def test_O_x_flag(self): def test_P_x_flag(self):
""" """
verify that the 'x' flag works when opening a file. verify that the 'x' flag works when opening a file.
""" """
@ -723,7 +763,7 @@ class SFTPTest (unittest.TestCase):
finally: finally:
sftp.unlink(FOLDER + '/unusual.txt') sftp.unlink(FOLDER + '/unusual.txt')
def test_P_utf8(self): def test_Q_utf8(self):
""" """
verify that unicode strings are encoded into utf8 correctly. verify that unicode strings are encoded into utf8 correctly.
""" """