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