1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17   
 18   
 19  """ 
 20  Client-mode SFTP support. 
 21  """ 
 22   
 23  from binascii import hexlify 
 24  import errno 
 25  import os 
 26  import stat 
 27  import threading 
 28  import time 
 29  import weakref 
 30   
 31  from paramiko.sftp import * 
 32  from paramiko.sftp_attr import SFTPAttributes 
 33  from paramiko.ssh_exception import SSHException 
 34  from paramiko.sftp_file import SFTPFile 
 35   
 36   
 38      """ 
 39      decode a string as ascii or utf8 if possible (as required by the sftp 
 40      protocol).  if neither works, just return a byte string because the server 
 41      probably doesn't know the filename's encoding. 
 42      """ 
 43      try: 
 44          return s.encode('ascii') 
 45      except UnicodeError: 
 46          try: 
 47              return s.decode('utf-8') 
 48          except UnicodeError: 
 49              return s 
  50   
 51   
 53      """ 
 54      SFTP client object.  C{SFTPClient} is used to open an sftp session across 
 55      an open ssh L{Transport} and do remote file operations. 
 56      """ 
 57   
 59          """ 
 60          Create an SFTP client from an existing L{Channel}.  The channel 
 61          should already have requested the C{"sftp"} subsystem. 
 62   
 63          An alternate way to create an SFTP client context is by using 
 64          L{from_transport}. 
 65   
 66          @param sock: an open L{Channel} using the C{"sftp"} subsystem 
 67          @type sock: L{Channel} 
 68   
 69          @raise SSHException: if there's an exception while negotiating 
 70              sftp 
 71          """ 
 72          BaseSFTP.__init__(self) 
 73          self.sock = sock 
 74          self.ultra_debug = False 
 75          self.request_number = 1 
 76           
 77          self._lock = threading.Lock() 
 78          self._cwd = None 
 79           
 80          self._expecting = weakref.WeakValueDictionary() 
 81          if type(sock) is Channel: 
 82               
 83              transport = self.sock.get_transport() 
 84              self.logger = util.get_logger(transport.get_log_channel() + '.sftp') 
 85              self.ultra_debug = transport.get_hexdump() 
 86          try: 
 87              server_version = self._send_version() 
 88          except EOFError, x: 
 89              raise SSHException('EOF during negotiation') 
 90          self._log(INFO, 'Opened sftp connection (server version %d)' % server_version) 
  91   
 93          """ 
 94          Create an SFTP client channel from an open L{Transport}. 
 95   
 96          @param t: an open L{Transport} which is already authenticated 
 97          @type t: L{Transport} 
 98          @return: a new L{SFTPClient} object, referring to an sftp session 
 99              (channel) across the transport 
100          @rtype: L{SFTPClient} 
101          """ 
102          chan = t.open_session() 
103          if chan is None: 
104              return None 
105          chan.invoke_subsystem('sftp') 
106          return cls(chan) 
 107      from_transport = classmethod(from_transport) 
108   
109 -    def _log(self, level, msg, *args): 
 110          if isinstance(msg, list): 
111              for m in msg: 
112                  super(SFTPClient, self)._log(level, "[chan %s] " + m, *([ self.sock.get_name() ] + list(args))) 
113          else: 
114              super(SFTPClient, self)._log(level, "[chan %s] " + msg, *([ self.sock.get_name() ] + list(args))) 
 115   
117          """ 
118          Close the SFTP session and its underlying channel. 
119   
120          @since: 1.4 
121          """ 
122          self._log(INFO, 'sftp session closed.') 
123          self.sock.close() 
 124   
126          """ 
127          Return the underlying L{Channel} object for this SFTP session.  This 
128          might be useful for doing things like setting a timeout on the channel. 
129   
130          @return: the SSH channel 
131          @rtype: L{Channel} 
132   
133          @since: 1.7.1 
134          """ 
135          return self.sock 
 136   
138          """ 
139          Return a list containing the names of the entries in the given C{path}. 
140          The list is in arbitrary order.  It does not include the special 
141          entries C{'.'} and C{'..'} even if they are present in the folder. 
142          This method is meant to mirror C{os.listdir} as closely as possible. 
143          For a list of full L{SFTPAttributes} objects, see L{listdir_attr}. 
144   
145          @param path: path to list (defaults to C{'.'}) 
146          @type path: str 
147          @return: list of filenames 
148          @rtype: list of str 
149          """ 
150          return [f.filename for f in self.listdir_attr(path)] 
 151   
153          """ 
154          Return a list containing L{SFTPAttributes} objects corresponding to 
155          files in the given C{path}.  The list is in arbitrary order.  It does 
156          not include the special entries C{'.'} and C{'..'} even if they are 
157          present in the folder. 
158   
159          The returned L{SFTPAttributes} objects will each have an additional 
160          field: C{longname}, which may contain a formatted string of the file's 
161          attributes, in unix format.  The content of this string will probably 
162          depend on the SFTP server implementation. 
163   
164          @param path: path to list (defaults to C{'.'}) 
165          @type path: str 
166          @return: list of attributes 
167          @rtype: list of L{SFTPAttributes} 
168   
169          @since: 1.2 
170          """ 
171          path = self._adjust_cwd(path) 
172          self._log(DEBUG, 'listdir(%r)' % path) 
173          t, msg = self._request(CMD_OPENDIR, path) 
174          if t != CMD_HANDLE: 
175              raise SFTPError('Expected handle') 
176          handle = msg.get_string() 
177          filelist = [] 
178          while True: 
179              try: 
180                  t, msg = self._request(CMD_READDIR, handle) 
181              except EOFError, e: 
182                   
183                  break 
184              if t != CMD_NAME: 
185                  raise SFTPError('Expected name response') 
186              count = msg.get_int() 
187              for i in range(count): 
188                  filename = _to_unicode(msg.get_string()) 
189                  longname = _to_unicode(msg.get_string()) 
190                  attr = SFTPAttributes._from_msg(msg, filename, longname) 
191                  if (filename != '.') and (filename != '..'): 
192                      filelist.append(attr) 
193          self._request(CMD_CLOSE, handle) 
194          return filelist 
 195   
196 -    def open(self, filename, mode='r', bufsize=-1): 
 197          """ 
198          Open a file on the remote server.  The arguments are the same as for 
199          python's built-in C{file} (aka C{open}).  A file-like object is 
200          returned, which closely mimics the behavior of a normal python file 
201          object. 
202   
203          The mode indicates how the file is to be opened: C{'r'} for reading, 
204          C{'w'} for writing (truncating an existing file), C{'a'} for appending, 
205          C{'r+'} for reading/writing, C{'w+'} for reading/writing (truncating an 
206          existing file), C{'a+'} for reading/appending.  The python C{'b'} flag 
207          is ignored, since SSH treats all files as binary.  The C{'U'} flag is 
208          supported in a compatible way. 
209   
210          Since 1.5.2, an C{'x'} flag indicates that the operation should only 
211          succeed if the file was created and did not previously exist.  This has 
212          no direct mapping to python's file flags, but is commonly known as the 
213          C{O_EXCL} flag in posix. 
214   
215          The file will be buffered in standard python style by default, but 
216          can be altered with the C{bufsize} parameter.  C{0} turns off 
217          buffering, C{1} uses line buffering, and any number greater than 1 
218          (C{>1}) uses that specific buffer size. 
219   
220          @param filename: name of the file to open 
221          @type filename: str 
222          @param mode: mode (python-style) to open in 
223          @type mode: str 
224          @param bufsize: desired buffering (-1 = default buffer size) 
225          @type bufsize: int 
226          @return: a file object representing the open file 
227          @rtype: SFTPFile 
228   
229          @raise IOError: if the file could not be opened. 
230          """ 
231          filename = self._adjust_cwd(filename) 
232          self._log(DEBUG, 'open(%r, %r)' % (filename, mode)) 
233          imode = 0 
234          if ('r' in mode) or ('+' in mode): 
235              imode |= SFTP_FLAG_READ 
236          if ('w' in mode) or ('+' in mode) or ('a' in mode): 
237              imode |= SFTP_FLAG_WRITE 
238          if ('w' in mode): 
239              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC 
240          if ('a' in mode): 
241              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND 
242          if ('x' in mode): 
243              imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL 
244          attrblock = SFTPAttributes() 
245          t, msg = self._request(CMD_OPEN, filename, imode, attrblock) 
246          if t != CMD_HANDLE: 
247              raise SFTPError('Expected handle') 
248          handle = msg.get_string() 
249          self._log(DEBUG, 'open(%r, %r) -> %s' % (filename, mode, hexlify(handle))) 
250          return SFTPFile(self, handle, mode, bufsize) 
 251   
252       
253      file = open 
254   
256          """ 
257          Remove the file at the given path.  This only works on files; for 
258          removing folders (directories), use L{rmdir}. 
259   
260          @param path: path (absolute or relative) of the file to remove 
261          @type path: str 
262   
263          @raise IOError: if the path refers to a folder (directory) 
264          """ 
265          path = self._adjust_cwd(path) 
266          self._log(DEBUG, 'remove(%r)' % path) 
267          self._request(CMD_REMOVE, path) 
 268   
269      unlink = remove 
270   
271 -    def rename(self, oldpath, newpath): 
 272          """ 
273          Rename a file or folder from C{oldpath} to C{newpath}. 
274   
275          @param oldpath: existing name of the file or folder 
276          @type oldpath: str 
277          @param newpath: new name for the file or folder 
278          @type newpath: str 
279   
280          @raise IOError: if C{newpath} is a folder, or something else goes 
281              wrong 
282          """ 
283          oldpath = self._adjust_cwd(oldpath) 
284          newpath = self._adjust_cwd(newpath) 
285          self._log(DEBUG, 'rename(%r, %r)' % (oldpath, newpath)) 
286          self._request(CMD_RENAME, oldpath, newpath) 
 287   
288 -    def mkdir(self, path, mode=0777): 
 289          """ 
290          Create a folder (directory) named C{path} with numeric mode C{mode}. 
291          The default mode is 0777 (octal).  On some systems, mode is ignored. 
292          Where it is used, the current umask value is first masked out. 
293   
294          @param path: name of the folder to create 
295          @type path: str 
296          @param mode: permissions (posix-style) for the newly-created folder 
297          @type mode: int 
298          """ 
299          path = self._adjust_cwd(path) 
300          self._log(DEBUG, 'mkdir(%r, %r)' % (path, mode)) 
301          attr = SFTPAttributes() 
302          attr.st_mode = mode 
303          self._request(CMD_MKDIR, path, attr) 
 304   
306          """ 
307          Remove the folder named C{path}. 
308   
309          @param path: name of the folder to remove 
310          @type path: str 
311          """ 
312          path = self._adjust_cwd(path) 
313          self._log(DEBUG, 'rmdir(%r)' % path) 
314          self._request(CMD_RMDIR, path) 
 315   
316 -    def stat(self, path): 
 317          """ 
318          Retrieve information about a file on the remote system.  The return 
319          value is an object whose attributes correspond to the attributes of 
320          python's C{stat} structure as returned by C{os.stat}, except that it 
321          contains fewer fields.  An SFTP server may return as much or as little 
322          info as it wants, so the results may vary from server to server. 
323   
324          Unlike a python C{stat} object, the result may not be accessed as a 
325          tuple.  This is mostly due to the author's slack factor. 
326   
327          The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid}, 
328          C{st_atime}, and C{st_mtime}. 
329   
330          @param path: the filename to stat 
331          @type path: str 
332          @return: an object containing attributes about the given file 
333          @rtype: SFTPAttributes 
334          """ 
335          path = self._adjust_cwd(path) 
336          self._log(DEBUG, 'stat(%r)' % path) 
337          t, msg = self._request(CMD_STAT, path) 
338          if t != CMD_ATTRS: 
339              raise SFTPError('Expected attributes') 
340          return SFTPAttributes._from_msg(msg) 
 341   
343          """ 
344          Retrieve information about a file on the remote system, without 
345          following symbolic links (shortcuts).  This otherwise behaves exactly 
346          the same as L{stat}. 
347   
348          @param path: the filename to stat 
349          @type path: str 
350          @return: an object containing attributes about the given file 
351          @rtype: SFTPAttributes 
352          """ 
353          path = self._adjust_cwd(path) 
354          self._log(DEBUG, 'lstat(%r)' % path) 
355          t, msg = self._request(CMD_LSTAT, path) 
356          if t != CMD_ATTRS: 
357              raise SFTPError('Expected attributes') 
358          return SFTPAttributes._from_msg(msg) 
 359   
361          """ 
362          Create a symbolic link (shortcut) of the C{source} path at 
363          C{destination}. 
364   
365          @param source: path of the original file 
366          @type source: str 
367          @param dest: path of the newly created symlink 
368          @type dest: str 
369          """ 
370          dest = self._adjust_cwd(dest) 
371          self._log(DEBUG, 'symlink(%r, %r)' % (source, dest)) 
372          if type(source) is unicode: 
373              source = source.encode('utf-8') 
374          self._request(CMD_SYMLINK, source, dest) 
 375   
376 -    def chmod(self, path, mode): 
 377          """ 
378          Change the mode (permissions) of a file.  The permissions are 
379          unix-style and identical to those used by python's C{os.chmod} 
380          function. 
381   
382          @param path: path of the file to change the permissions of 
383          @type path: str 
384          @param mode: new permissions 
385          @type mode: int 
386          """ 
387          path = self._adjust_cwd(path) 
388          self._log(DEBUG, 'chmod(%r, %r)' % (path, mode)) 
389          attr = SFTPAttributes() 
390          attr.st_mode = mode 
391          self._request(CMD_SETSTAT, path, attr) 
 392   
393 -    def chown(self, path, uid, gid): 
 394          """ 
395          Change the owner (C{uid}) and group (C{gid}) of a file.  As with 
396          python's C{os.chown} function, you must pass both arguments, so if you 
397          only want to change one, use L{stat} first to retrieve the current 
398          owner and group. 
399   
400          @param path: path of the file to change the owner and group of 
401          @type path: str 
402          @param uid: new owner's uid 
403          @type uid: int 
404          @param gid: new group id 
405          @type gid: int 
406          """ 
407          path = self._adjust_cwd(path) 
408          self._log(DEBUG, 'chown(%r, %r, %r)' % (path, uid, gid)) 
409          attr = SFTPAttributes() 
410          attr.st_uid, attr.st_gid = uid, gid 
411          self._request(CMD_SETSTAT, path, attr) 
 412   
413 -    def utime(self, path, times): 
 414          """ 
415          Set the access and modified times of the file specified by C{path}.  If 
416          C{times} is C{None}, then the file's access and modified times are set 
417          to the current time.  Otherwise, C{times} must be a 2-tuple of numbers, 
418          of the form C{(atime, mtime)}, which is used to set the access and 
419          modified times, respectively.  This bizarre API is mimicked from python 
420          for the sake of consistency -- I apologize. 
421   
422          @param path: path of the file to modify 
423          @type path: str 
424          @param times: C{None} or a tuple of (access time, modified time) in 
425              standard internet epoch time (seconds since 01 January 1970 GMT) 
426          @type times: tuple(int) 
427          """ 
428          path = self._adjust_cwd(path) 
429          if times is None: 
430              times = (time.time(), time.time()) 
431          self._log(DEBUG, 'utime(%r, %r)' % (path, times)) 
432          attr = SFTPAttributes() 
433          attr.st_atime, attr.st_mtime = times 
434          self._request(CMD_SETSTAT, path, attr) 
 435   
437          """ 
438          Change the size of the file specified by C{path}.  This usually extends 
439          or shrinks the size of the file, just like the C{truncate()} method on 
440          python file objects. 
441   
442          @param path: path of the file to modify 
443          @type path: str 
444          @param size: the new size of the file 
445          @type size: int or long 
446          """ 
447          path = self._adjust_cwd(path) 
448          self._log(DEBUG, 'truncate(%r, %r)' % (path, size)) 
449          attr = SFTPAttributes() 
450          attr.st_size = size 
451          self._request(CMD_SETSTAT, path, attr) 
 452   
454          """ 
455          Return the target of a symbolic link (shortcut).  You can use 
456          L{symlink} to create these.  The result may be either an absolute or 
457          relative pathname. 
458   
459          @param path: path of the symbolic link file 
460          @type path: str 
461          @return: target path 
462          @rtype: str 
463          """ 
464          path = self._adjust_cwd(path) 
465          self._log(DEBUG, 'readlink(%r)' % path) 
466          t, msg = self._request(CMD_READLINK, path) 
467          if t != CMD_NAME: 
468              raise SFTPError('Expected name response') 
469          count = msg.get_int() 
470          if count == 0: 
471              return None 
472          if count != 1: 
473              raise SFTPError('Readlink returned %d results' % count) 
474          return _to_unicode(msg.get_string()) 
 475   
477          """ 
478          Return the normalized path (on the server) of a given path.  This 
479          can be used to quickly resolve symbolic links or determine what the 
480          server is considering to be the "current folder" (by passing C{'.'} 
481          as C{path}). 
482   
483          @param path: path to be normalized 
484          @type path: str 
485          @return: normalized form of the given path 
486          @rtype: str 
487   
488          @raise IOError: if the path can't be resolved on the server 
489          """ 
490          path = self._adjust_cwd(path) 
491          self._log(DEBUG, 'normalize(%r)' % path) 
492          t, msg = self._request(CMD_REALPATH, path) 
493          if t != CMD_NAME: 
494              raise SFTPError('Expected name response') 
495          count = msg.get_int() 
496          if count != 1: 
497              raise SFTPError('Realpath returned %d results' % count) 
498          return _to_unicode(msg.get_string()) 
 499   
501          """ 
502          Change the "current directory" of this SFTP session.  Since SFTP 
503          doesn't really have the concept of a current working directory, this 
504          is emulated by paramiko.  Once you use this method to set a working 
505          directory, all operations on this SFTPClient object will be relative 
506          to that path. You can pass in C{None} to stop using a current working 
507          directory. 
508   
509          @param path: new current working directory 
510          @type path: str 
511   
512          @raise IOError: if the requested path doesn't exist on the server 
513   
514          @since: 1.4 
515          """ 
516          if path is None: 
517              self._cwd = None 
518              return 
519          if not stat.S_ISDIR(self.stat(path).st_mode): 
520              raise SFTPError(errno.ENOTDIR, "%s: %s" % (os.strerror(errno.ENOTDIR), path)) 
521          self._cwd = self.normalize(path).encode('utf-8') 
 522   
524          """ 
525          Return the "current working directory" for this SFTP session, as 
526          emulated by paramiko.  If no directory has been set with L{chdir}, 
527          this method will return C{None}. 
528   
529          @return: the current working directory on the server, or C{None} 
530          @rtype: str 
531   
532          @since: 1.4 
533          """ 
534          return self._cwd 
 535   
536 -    def put(self, localpath, remotepath, callback=None): 
 537          """ 
538          Copy a local file (C{localpath}) to the SFTP server as C{remotepath}. 
539          Any exception raised by operations will be passed through.  This 
540          method is primarily provided as a convenience. 
541   
542          The SFTP operations use pipelining for speed. 
543   
544          @param localpath: the local file to copy 
545          @type localpath: str 
546          @param remotepath: the destination path on the SFTP server 
547          @type remotepath: str 
548          @param callback: optional callback function that accepts the bytes 
549              transferred so far and the total bytes to be transferred 
550              (since 1.7.4) 
551          @type callback: function(int, int) 
552          @return: an object containing attributes about the given file 
553              (since 1.7.4) 
554          @rtype: SFTPAttributes 
555   
556          @since: 1.4 
557          """ 
558          file_size = os.stat(localpath).st_size 
559          fl = file(localpath, 'rb') 
560          try: 
561              fr = self.file(remotepath, 'wb') 
562              fr.set_pipelined(True) 
563              size = 0 
564              try: 
565                  while True: 
566                      data = fl.read(32768) 
567                      if len(data) == 0: 
568                          break 
569                      fr.write(data) 
570                      size += len(data) 
571                      if callback is not None: 
572                          callback(size, file_size) 
573              finally: 
574                  fr.close() 
575          finally: 
576              fl.close() 
577          s = self.stat(remotepath) 
578          if s.st_size != size: 
579              raise IOError('size mismatch in put!  %d != %d' % (s.st_size, size)) 
580          return s 
 581   
582 -    def get(self, remotepath, localpath, callback=None): 
 583          """ 
584          Copy a remote file (C{remotepath}) from the SFTP server to the local 
585          host as C{localpath}.  Any exception raised by operations will be 
586          passed through.  This method is primarily provided as a convenience. 
587   
588          @param remotepath: the remote file to copy 
589          @type remotepath: str 
590          @param localpath: the destination path on the local host 
591          @type localpath: str 
592          @param callback: optional callback function that accepts the bytes 
593              transferred so far and the total bytes to be transferred 
594              (since 1.7.4) 
595          @type callback: function(int, int) 
596   
597          @since: 1.4 
598          """ 
599          fr = self.file(remotepath, 'rb') 
600          file_size = self.stat(remotepath).st_size 
601          fr.prefetch() 
602          try: 
603              fl = file(localpath, 'wb') 
604              try: 
605                  size = 0 
606                  while True: 
607                      data = fr.read(32768) 
608                      if len(data) == 0: 
609                          break 
610                      fl.write(data) 
611                      size += len(data) 
612                      if callback is not None: 
613                          callback(size, file_size) 
614              finally: 
615                  fl.close() 
616          finally: 
617              fr.close() 
618          s = os.stat(localpath) 
619          if s.st_size != size: 
620              raise IOError('size mismatch in get!  %d != %d' % (s.st_size, size)) 
 621   
622   
623       
624   
625   
627          num = self._async_request(type(None), t, *arg) 
628          return self._read_response(num) 
 629   
631           
632          self._lock.acquire() 
633          try: 
634              msg = Message() 
635              msg.add_int(self.request_number) 
636              for item in arg: 
637                  if type(item) is int: 
638                      msg.add_int(item) 
639                  elif type(item) is long: 
640                      msg.add_int64(item) 
641                  elif type(item) is str: 
642                      msg.add_string(item) 
643                  elif type(item) is SFTPAttributes: 
644                      item._pack(msg) 
645                  else: 
646                      raise Exception('unknown type for %r type %r' % (item, type(item))) 
647              num = self.request_number 
648              self._expecting[num] = fileobj 
649              self._send_packet(t, str(msg)) 
650              self.request_number += 1 
651          finally: 
652              self._lock.release() 
653          return num 
 654   
656          while True: 
657              try: 
658                  t, data = self._read_packet() 
659              except EOFError, e: 
660                  raise SSHException('Server connection dropped: %s' % (str(e),)) 
661              msg = Message(data) 
662              num = msg.get_int() 
663              if num not in self._expecting: 
664                   
665                  self._log(DEBUG, 'Unexpected response #%d' % (num,)) 
666                  if waitfor is None: 
667                       
668                      break 
669                  continue 
670              fileobj = self._expecting[num] 
671              del self._expecting[num] 
672              if num == waitfor: 
673                   
674                  if t == CMD_STATUS: 
675                      self._convert_status(msg) 
676                  return t, msg 
677              if fileobj is not type(None): 
678                  fileobj._async_response(t, msg) 
679              if waitfor is None: 
680                   
681                  break 
682          return (None, None) 
 683   
685          while fileobj in self._expecting.values(): 
686              self._read_response() 
687              fileobj._check_exception() 
 688   
690          """ 
691          Raises EOFError or IOError on error status; otherwise does nothing. 
692          """ 
693          code = msg.get_int() 
694          text = msg.get_string() 
695          if code == SFTP_OK: 
696              return 
697          elif code == SFTP_EOF: 
698              raise EOFError(text) 
699          elif code == SFTP_NO_SUCH_FILE: 
700               
701              raise IOError(errno.ENOENT, text) 
702          elif code == SFTP_PERMISSION_DENIED: 
703              raise IOError(errno.EACCES, text) 
704          else: 
705              raise IOError(text) 
 706   
708          """ 
709          Return an adjusted path if we're emulating a "current working 
710          directory" for the server. 
711          """ 
712          if type(path) is unicode: 
713              path = path.encode('utf-8') 
714          if self._cwd is None: 
715              return path 
716          if (len(path) > 0) and (path[0] == '/'): 
717               
718              return path 
719          if self._cwd == '/': 
720              return self._cwd + path 
721          return self._cwd + '/' + path 
  722   
723   
724 -class SFTP (SFTPClient): 
 725      "an alias for L{SFTPClient} for backwards compatability" 
726      pass 
 727