1# -*- Mode: Python -*-
2#       $Id: filesys.py,v 1.9 2003/12/24 16:10:56 akuchling Exp $
3#       Author: Sam Rushing <rushing@nightmare.com>
4#
5# Generic filesystem interface.
6#
7
8# We want to provide a complete wrapper around any and all
9# filesystem operations.
10
11# this class is really just for documentation,
12# identifying the API for a filesystem object.
13
14# opening files for reading, and listing directories, should
15# return a producer.
16
17from supervisor.compat import long
18
19class abstract_filesystem:
20    def __init__ (self):
21        pass
22
23    def current_directory (self):
24        """Return a string representing the current directory."""
25        pass
26
27    def listdir (self, path, long=0):
28        """Return a listing of the directory at 'path' The empty string
29        indicates the current directory.  If 'long' is set, instead
30        return a list of (name, stat_info) tuples
31        """
32        pass
33
34    def open (self, path, mode):
35        """Return an open file object"""
36        pass
37
38    def stat (self, path):
39        """Return the equivalent of os.stat() on the given path."""
40        pass
41
42    def isdir (self, path):
43        """Does the path represent a directory?"""
44        pass
45
46    def isfile (self, path):
47        """Does the path represent a plain file?"""
48        pass
49
50    def cwd (self, path):
51        """Change the working directory."""
52        pass
53
54    def cdup (self):
55        """Change to the parent of the current directory."""
56        pass
57
58
59    def longify (self, path):
60        """Return a 'long' representation of the filename
61        [for the output of the LIST command]"""
62        pass
63
64# standard wrapper around a unix-like filesystem, with a 'false root'
65# capability.
66
67# security considerations: can symbolic links be used to 'escape' the
68# root?  should we allow it?  if not, then we could scan the
69# filesystem on startup, but that would not help if they were added
70# later.  We will probably need to check for symlinks in the cwd method.
71
72# what to do if wd is an invalid directory?
73
74import os
75import stat
76import re
77
78def safe_stat (path):
79    try:
80        return path, os.stat (path)
81    except:
82        return None
83
84class os_filesystem:
85    path_module = os.path
86
87    # set this to zero if you want to disable pathname globbing.
88    # [we currently don't glob, anyway]
89    do_globbing = 1
90
91    def __init__ (self, root, wd='/'):
92        self.root = root
93        self.wd = wd
94
95    def current_directory (self):
96        return self.wd
97
98    def isfile (self, path):
99        p = self.normalize (self.path_module.join (self.wd, path))
100        return self.path_module.isfile (self.translate(p))
101
102    def isdir (self, path):
103        p = self.normalize (self.path_module.join (self.wd, path))
104        return self.path_module.isdir (self.translate(p))
105
106    def cwd (self, path):
107        p = self.normalize (self.path_module.join (self.wd, path))
108        translated_path = self.translate(p)
109        if not self.path_module.isdir (translated_path):
110            return 0
111        else:
112            old_dir = os.getcwd()
113            # temporarily change to that directory, in order
114            # to see if we have permission to do so.
115            can = 0
116            try:
117                try:
118                    os.chdir (translated_path)
119                    can = 1
120                    self.wd = p
121                except:
122                    pass
123            finally:
124                if can:
125                    os.chdir (old_dir)
126            return can
127
128    def cdup (self):
129        return self.cwd ('..')
130
131    def listdir (self, path, long=0):
132        p = self.translate (path)
133        # I think we should glob, but limit it to the current
134        # directory only.
135        ld = os.listdir (p)
136        if not long:
137            return list_producer (ld, None)
138        else:
139            old_dir = os.getcwd()
140            try:
141                os.chdir (p)
142                # if os.stat fails we ignore that file.
143                result = [_f for _f in map (safe_stat, ld) if _f]
144            finally:
145                os.chdir (old_dir)
146            return list_producer (result, self.longify)
147
148    # TODO: implement a cache w/timeout for stat()
149    def stat (self, path):
150        p = self.translate (path)
151        return os.stat (p)
152
153    def open (self, path, mode):
154        p = self.translate (path)
155        return open (p, mode)
156
157    def unlink (self, path):
158        p = self.translate (path)
159        return os.unlink (p)
160
161    def mkdir (self, path):
162        p = self.translate (path)
163        return os.mkdir (p)
164
165    def rmdir (self, path):
166        p = self.translate (path)
167        return os.rmdir (p)
168
169    def rename(self, src, dst):
170        return os.rename(self.translate(src),self.translate(dst))
171
172    # utility methods
173    def normalize (self, path):
174        # watch for the ever-sneaky '/+' path element
175        path = re.sub('/+', '/', path)
176        p = self.path_module.normpath (path)
177        # remove 'dangling' cdup's.
178        if len(p) > 2 and p[:3] == '/..':
179            p = '/'
180        return p
181
182    def translate (self, path):
183        # we need to join together three separate
184        # path components, and do it safely.
185        # <real_root>/<current_directory>/<path>
186        # use the operating system's path separator.
187        path = os.sep.join(path.split('/'))
188        p = self.normalize (self.path_module.join (self.wd, path))
189        p = self.normalize (self.path_module.join (self.root, p[1:]))
190        return p
191
192    def longify (self, path_stat_info_tuple):
193        (path, stat_info) = path_stat_info_tuple
194        return unix_longify (path, stat_info)
195
196    def __repr__ (self):
197        return '<unix-style fs root:%s wd:%s>' % (
198                self.root,
199                self.wd
200                )
201
202if os.name == 'posix':
203
204    class unix_filesystem (os_filesystem):
205        pass
206
207    class schizophrenic_unix_filesystem (os_filesystem):
208        PROCESS_UID     = os.getuid()
209        PROCESS_EUID    = os.geteuid()
210        PROCESS_GID     = os.getgid()
211        PROCESS_EGID    = os.getegid()
212
213        def __init__ (self, root, wd='/', persona=(None, None)):
214            os_filesystem.__init__ (self, root, wd)
215            self.persona = persona
216
217        def become_persona (self):
218            if self.persona != (None, None):
219                uid, gid = self.persona
220                # the order of these is important!
221                os.setegid (gid)
222                os.seteuid (uid)
223
224        def become_nobody (self):
225            if self.persona != (None, None):
226                os.seteuid (self.PROCESS_UID)
227                os.setegid (self.PROCESS_GID)
228
229        # cwd, cdup, open, listdir
230        def cwd (self, path):
231            try:
232                self.become_persona()
233                return os_filesystem.cwd (self, path)
234            finally:
235                self.become_nobody()
236
237        def cdup (self):
238            try:
239                self.become_persona()
240                return os_filesystem.cdup (self)
241            finally:
242                self.become_nobody()
243
244        def open (self, filename, mode):
245            try:
246                self.become_persona()
247                return os_filesystem.open (self, filename, mode)
248            finally:
249                self.become_nobody()
250
251        def listdir (self, path, long=0):
252            try:
253                self.become_persona()
254                return os_filesystem.listdir (self, path, long)
255            finally:
256                self.become_nobody()
257
258# For the 'real' root, we could obtain a list of drives, and then
259# use that.  Doesn't win32 provide such a 'real' filesystem?
260# [yes, I think something like this "\\.\c\windows"]
261
262class msdos_filesystem (os_filesystem):
263    def longify (self, path_stat_info_tuple):
264        (path, stat_info) = path_stat_info_tuple
265        return msdos_longify (path, stat_info)
266
267# A merged filesystem will let you plug other filesystems together.
268# We really need the equivalent of a 'mount' capability - this seems
269# to be the most general idea.  So you'd use a 'mount' method to place
270# another filesystem somewhere in the hierarchy.
271
272# Note: this is most likely how I will handle ~user directories
273# with the http server.
274
275class merged_filesystem:
276    def __init__ (self, *fsys):
277        pass
278
279# this matches the output of NT's ftp server (when in
280# MSDOS mode) exactly.
281
282def msdos_longify (file, stat_info):
283    if stat.S_ISDIR (stat_info[stat.ST_MODE]):
284        dir = '<DIR>'
285    else:
286        dir = '     '
287    date = msdos_date (stat_info[stat.ST_MTIME])
288    return '%s       %s %8d %s' % (
289            date,
290            dir,
291            stat_info[stat.ST_SIZE],
292            file
293            )
294
295def msdos_date (t):
296    try:
297        info = time.gmtime (t)
298    except:
299        info = time.gmtime (0)
300    # year, month, day, hour, minute, second, ...
301    hour = info[3]
302    if hour > 11:
303        merid = 'PM'
304        hour -= 12
305    else:
306        merid = 'AM'
307    return '%02d-%02d-%02d  %02d:%02d%s' % (
308            info[1],
309            info[2],
310            info[0]%100,
311            hour,
312            info[4],
313            merid
314            )
315
316months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
317                  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
318
319mode_table = {
320        '0':'---',
321        '1':'--x',
322        '2':'-w-',
323        '3':'-wx',
324        '4':'r--',
325        '5':'r-x',
326        '6':'rw-',
327        '7':'rwx'
328        }
329
330import time
331
332def unix_longify (file, stat_info):
333    # for now, only pay attention to the lower bits
334    mode = ('%o' % stat_info[stat.ST_MODE])[-3:]
335    mode = ''.join([mode_table[x] for x in mode])
336    if stat.S_ISDIR (stat_info[stat.ST_MODE]):
337        dirchar = 'd'
338    else:
339        dirchar = '-'
340    date = ls_date (long(time.time()), stat_info[stat.ST_MTIME])
341    return '%s%s %3d %-8d %-8d %8d %s %s' % (
342            dirchar,
343            mode,
344            stat_info[stat.ST_NLINK],
345            stat_info[stat.ST_UID],
346            stat_info[stat.ST_GID],
347            stat_info[stat.ST_SIZE],
348            date,
349            file
350            )
351
352# Emulate the unix 'ls' command's date field.
353# it has two formats - if the date is more than 180
354# days in the past, then it's like this:
355# Oct 19  1995
356# otherwise, it looks like this:
357# Oct 19 17:33
358
359def ls_date (now, t):
360    try:
361        info = time.gmtime (t)
362    except:
363        info = time.gmtime (0)
364    # 15,600,000 == 86,400 * 180
365    if (now - t) > 15600000:
366        return '%s %2d  %d' % (
367                months[info[1]-1],
368                info[2],
369                info[0]
370                )
371    else:
372        return '%s %2d %02d:%02d' % (
373                months[info[1]-1],
374                info[2],
375                info[3],
376                info[4]
377                )
378
379# ===========================================================================
380# Producers
381# ===========================================================================
382
383class list_producer:
384    def __init__ (self, list, func=None):
385        self.list = list
386        self.func = func
387
388    # this should do a pushd/popd
389    def more (self):
390        if not self.list:
391            return ''
392        else:
393            # do a few at a time
394            bunch = self.list[:50]
395            if self.func is not None:
396                bunch = map (self.func, bunch)
397            self.list = self.list[50:]
398            return '\r\n'.join(bunch) + '\r\n'
399
400