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