1# Copyright (C) 2003-2006 Robey Pointer <robeypointer@gmail.com> 2# 3# This file is part of paramiko. 4# 5# Paramiko is free software; you can redistribute it and/or modify it under the 6# terms of the GNU Lesser General Public License as published by the Free 7# Software Foundation; either version 2.1 of the License, or (at your option) 8# any later version. 9# 10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13# details. 14# 15# You should have received a copy of the GNU Lesser General Public License 16# along with Paramiko; if not, write to the Free Software Foundation, Inc., 17# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 19import stat 20import time 21from paramiko.common import x80000000, o700, o70, xffffffff 22from paramiko.py3compat import long, b 23 24 25class SFTPAttributes(object): 26 """ 27 Representation of the attributes of a file (or proxied file) for SFTP in 28 client or server mode. It attemps to mirror the object returned by 29 `os.stat` as closely as possible, so it may have the following fields, 30 with the same meanings as those returned by an `os.stat` object: 31 32 - ``st_size`` 33 - ``st_uid`` 34 - ``st_gid`` 35 - ``st_mode`` 36 - ``st_atime`` 37 - ``st_mtime`` 38 39 Because SFTP allows flags to have other arbitrary named attributes, these 40 are stored in a dict named ``attr``. Occasionally, the filename is also 41 stored, in ``filename``. 42 """ 43 44 FLAG_SIZE = 1 45 FLAG_UIDGID = 2 46 FLAG_PERMISSIONS = 4 47 FLAG_AMTIME = 8 48 FLAG_EXTENDED = x80000000 49 50 def __init__(self): 51 """ 52 Create a new (empty) SFTPAttributes object. All fields will be empty. 53 """ 54 self._flags = 0 55 self.st_size = None 56 self.st_uid = None 57 self.st_gid = None 58 self.st_mode = None 59 self.st_atime = None 60 self.st_mtime = None 61 self.attr = {} 62 63 @classmethod 64 def from_stat(cls, obj, filename=None): 65 """ 66 Create an `.SFTPAttributes` object from an existing ``stat`` object (an 67 object returned by `os.stat`). 68 69 :param object obj: an object returned by `os.stat` (or equivalent). 70 :param str filename: the filename associated with this file. 71 :return: new `.SFTPAttributes` object with the same attribute fields. 72 """ 73 attr = cls() 74 attr.st_size = obj.st_size 75 attr.st_uid = obj.st_uid 76 attr.st_gid = obj.st_gid 77 attr.st_mode = obj.st_mode 78 attr.st_atime = obj.st_atime 79 attr.st_mtime = obj.st_mtime 80 if filename is not None: 81 attr.filename = filename 82 return attr 83 84 def __repr__(self): 85 return "<SFTPAttributes: {}>".format(self._debug_str()) 86 87 # ...internals... 88 @classmethod 89 def _from_msg(cls, msg, filename=None, longname=None): 90 attr = cls() 91 attr._unpack(msg) 92 if filename is not None: 93 attr.filename = filename 94 if longname is not None: 95 attr.longname = longname 96 return attr 97 98 def _unpack(self, msg): 99 self._flags = msg.get_int() 100 if self._flags & self.FLAG_SIZE: 101 self.st_size = msg.get_int64() 102 if self._flags & self.FLAG_UIDGID: 103 self.st_uid = msg.get_int() 104 self.st_gid = msg.get_int() 105 if self._flags & self.FLAG_PERMISSIONS: 106 self.st_mode = msg.get_int() 107 if self._flags & self.FLAG_AMTIME: 108 self.st_atime = msg.get_int() 109 self.st_mtime = msg.get_int() 110 if self._flags & self.FLAG_EXTENDED: 111 count = msg.get_int() 112 for i in range(count): 113 self.attr[msg.get_string()] = msg.get_string() 114 115 def _pack(self, msg): 116 self._flags = 0 117 if self.st_size is not None: 118 self._flags |= self.FLAG_SIZE 119 if (self.st_uid is not None) and (self.st_gid is not None): 120 self._flags |= self.FLAG_UIDGID 121 if self.st_mode is not None: 122 self._flags |= self.FLAG_PERMISSIONS 123 if (self.st_atime is not None) and (self.st_mtime is not None): 124 self._flags |= self.FLAG_AMTIME 125 if len(self.attr) > 0: 126 self._flags |= self.FLAG_EXTENDED 127 msg.add_int(self._flags) 128 if self._flags & self.FLAG_SIZE: 129 msg.add_int64(self.st_size) 130 if self._flags & self.FLAG_UIDGID: 131 msg.add_int(self.st_uid) 132 msg.add_int(self.st_gid) 133 if self._flags & self.FLAG_PERMISSIONS: 134 msg.add_int(self.st_mode) 135 if self._flags & self.FLAG_AMTIME: 136 # throw away any fractional seconds 137 msg.add_int(long(self.st_atime)) 138 msg.add_int(long(self.st_mtime)) 139 if self._flags & self.FLAG_EXTENDED: 140 msg.add_int(len(self.attr)) 141 for key, val in self.attr.items(): 142 msg.add_string(key) 143 msg.add_string(val) 144 return 145 146 def _debug_str(self): 147 out = "[ " 148 if self.st_size is not None: 149 out += "size={} ".format(self.st_size) 150 if (self.st_uid is not None) and (self.st_gid is not None): 151 out += "uid={} gid={} ".format(self.st_uid, self.st_gid) 152 if self.st_mode is not None: 153 out += "mode=" + oct(self.st_mode) + " " 154 if (self.st_atime is not None) and (self.st_mtime is not None): 155 out += "atime={} mtime={} ".format(self.st_atime, self.st_mtime) 156 for k, v in self.attr.items(): 157 out += '"{}"={!r} '.format(str(k), v) 158 out += "]" 159 return out 160 161 @staticmethod 162 def _rwx(n, suid, sticky=False): 163 if suid: 164 suid = 2 165 out = "-r"[n >> 2] + "-w"[(n >> 1) & 1] 166 if sticky: 167 out += "-xTt"[suid + (n & 1)] 168 else: 169 out += "-xSs"[suid + (n & 1)] 170 return out 171 172 def __str__(self): 173 """create a unix-style long description of the file (like ls -l)""" 174 if self.st_mode is not None: 175 kind = stat.S_IFMT(self.st_mode) 176 if kind == stat.S_IFIFO: 177 ks = "p" 178 elif kind == stat.S_IFCHR: 179 ks = "c" 180 elif kind == stat.S_IFDIR: 181 ks = "d" 182 elif kind == stat.S_IFBLK: 183 ks = "b" 184 elif kind == stat.S_IFREG: 185 ks = "-" 186 elif kind == stat.S_IFLNK: 187 ks = "l" 188 elif kind == stat.S_IFSOCK: 189 ks = "s" 190 else: 191 ks = "?" 192 ks += self._rwx( 193 (self.st_mode & o700) >> 6, self.st_mode & stat.S_ISUID 194 ) 195 ks += self._rwx( 196 (self.st_mode & o70) >> 3, self.st_mode & stat.S_ISGID 197 ) 198 ks += self._rwx( 199 self.st_mode & 7, self.st_mode & stat.S_ISVTX, True 200 ) 201 else: 202 ks = "?---------" 203 # compute display date 204 if (self.st_mtime is None) or (self.st_mtime == xffffffff): 205 # shouldn't really happen 206 datestr = "(unknown date)" 207 else: 208 if abs(time.time() - self.st_mtime) > 15552000: 209 # (15552000 = 6 months) 210 datestr = time.strftime( 211 "%d %b %Y", time.localtime(self.st_mtime) 212 ) 213 else: 214 datestr = time.strftime( 215 "%d %b %H:%M", time.localtime(self.st_mtime) 216 ) 217 filename = getattr(self, "filename", "?") 218 219 # not all servers support uid/gid 220 uid = self.st_uid 221 gid = self.st_gid 222 size = self.st_size 223 if uid is None: 224 uid = 0 225 if gid is None: 226 gid = 0 227 if size is None: 228 size = 0 229 230 # TODO: not sure this actually worked as expected beforehand, leaving 231 # it untouched for the time being, re: .format() upgrade, until someone 232 # has time to doublecheck 233 return "%s 1 %-8d %-8d %8d %-12s %s" % ( 234 ks, 235 uid, 236 gid, 237 size, 238 datestr, 239 filename, 240 ) 241 242 def asbytes(self): 243 return b(str(self)) 244