X-FTP
[x93.git/.git] / xftp.git / ext / pyftpdlib / filesystems.py
1 # Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.\r
2 # Use of this source code is governed by MIT license that can be\r
3 # found in the LICENSE file.\r
4 \r
5 import os\r
6 import stat\r
7 import tempfile\r
8 import time\r
9 try:\r
10     from stat import filemode as _filemode  # PY 3.3\r
11 except ImportError:\r
12     from tarfile import filemode as _filemode\r
13 try:\r
14     import pwd\r
15     import grp\r
16 except ImportError:\r
17     pwd = grp = None\r
18 try:\r
19     from os import scandir  # py 3.5\r
20 except ImportError:\r
21     try:\r
22         from scandir import scandir  # requires "pip install scandir"\r
23     except ImportError:\r
24         scandir = None\r
25 \r
26 from ._compat import PY3\r
27 from ._compat import u\r
28 from ._compat import unicode\r
29 \r
30 \r
31 __all__ = ['FilesystemError', 'AbstractedFS']\r
32 \r
33 \r
34 _months_map = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',\r
35                7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}\r
36 \r
37 \r
38 def _memoize(fun):\r
39     """A simple memoize decorator for functions supporting (hashable)\r
40     positional arguments.\r
41     """\r
42     def wrapper(*args, **kwargs):\r
43         key = (args, frozenset(sorted(kwargs.items())))\r
44         try:\r
45             return cache[key]\r
46         except KeyError:\r
47             ret = cache[key] = fun(*args, **kwargs)\r
48             return ret\r
49 \r
50     cache = {}\r
51     return wrapper\r
52 \r
53 \r
54 # ===================================================================\r
55 # --- custom exceptions\r
56 # ===================================================================\r
57 \r
58 class FilesystemError(Exception):\r
59     """Custom class for filesystem-related exceptions.\r
60     You can raise this from an AbstractedFS subclass in order to\r
61     send a customized error string to the client.\r
62     """\r
63 \r
64 \r
65 # ===================================================================\r
66 # --- base class\r
67 # ===================================================================\r
68 \r
69 class AbstractedFS(object):\r
70     """A class used to interact with the file system, providing a\r
71     cross-platform interface compatible with both Windows and\r
72     UNIX style filesystems where all paths use "/" separator.\r
73 \r
74     AbstractedFS distinguishes between "real" filesystem paths and\r
75     "virtual" ftp paths emulating a UNIX chroot jail where the user\r
76     can not escape its home directory (example: real "/home/user"\r
77     path will be seen as "/" by the client)\r
78 \r
79     It also provides some utility methods and wraps around all os.*\r
80     calls involving operations against the filesystem like creating\r
81     files or removing directories.\r
82 \r
83     FilesystemError exception can be raised from within any of\r
84     the methods below in order to send a customized error string\r
85     to the client.\r
86     """\r
87 \r
88     def __init__(self, root, cmd_channel):\r
89         """\r
90          - (str) root: the user "real" home directory (e.g. '/home/user')\r
91          - (instance) cmd_channel: the FTPHandler class instance\r
92         """\r
93         assert isinstance(root, unicode)\r
94         # Set initial current working directory.\r
95         # By default initial cwd is set to "/" to emulate a chroot jail.\r
96         # If a different behavior is desired (e.g. initial cwd = root,\r
97         # to reflect the real filesystem) users overriding this class\r
98         # are responsible to set _cwd attribute as necessary.\r
99         self._cwd = u('/')\r
100         self._root = root\r
101         self.cmd_channel = cmd_channel\r
102 \r
103     @property\r
104     def root(self):\r
105         """The user home directory."""\r
106         return self._root\r
107 \r
108     @property\r
109     def cwd(self):\r
110         """The user current working directory."""\r
111         return self._cwd\r
112 \r
113     @root.setter\r
114     def root(self, path):\r
115         assert isinstance(path, unicode), path\r
116         self._root = path\r
117 \r
118     @cwd.setter\r
119     def cwd(self, path):\r
120         assert isinstance(path, unicode), path\r
121         self._cwd = path\r
122 \r
123     # --- Pathname / conversion utilities\r
124 \r
125     def ftpnorm(self, ftppath):\r
126         """Normalize a "virtual" ftp pathname (typically the raw string\r
127         coming from client) depending on the current working directory.\r
128 \r
129         Example (having "/foo" as current working directory):\r
130         >>> ftpnorm('bar')\r
131         '/foo/bar'\r
132 \r
133         Note: directory separators are system independent ("/").\r
134         Pathname returned is always absolutized.\r
135         """\r
136         assert isinstance(ftppath, unicode), ftppath\r
137         if os.path.isabs(ftppath):\r
138             p = os.path.normpath(ftppath)\r
139         else:\r
140             p = os.path.normpath(os.path.join(self.cwd, ftppath))\r
141         # normalize string in a standard web-path notation having '/'\r
142         # as separator.\r
143         if os.sep == "\\":\r
144             p = p.replace("\\", "/")\r
145         # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we\r
146         # don't need them.  In case we get an UNC path we collapse\r
147         # redundant separators appearing at the beginning of the string\r
148         while p[:2] == '//':\r
149             p = p[1:]\r
150         # Anti path traversal: don't trust user input, in the event\r
151         # that self.cwd is not absolute, return "/" as a safety measure.\r
152         # This is for extra protection, maybe not really necessary.\r
153         if not os.path.isabs(p):\r
154             p = u("/")\r
155         return p\r
156 \r
157     def ftp2fs(self, ftppath):\r
158         """Translate a "virtual" ftp pathname (typically the raw string\r
159         coming from client) into equivalent absolute "real" filesystem\r
160         pathname.\r
161 \r
162         Example (having "/home/user" as root directory):\r
163         >>> ftp2fs("foo")\r
164         '/home/user/foo'\r
165 \r
166         Note: directory separators are system dependent.\r
167         """\r
168         assert isinstance(ftppath, unicode), ftppath\r
169         # as far as I know, it should always be path traversal safe...\r
170         if os.path.normpath(self.root) == os.sep:\r
171             return os.path.normpath(self.ftpnorm(ftppath))\r
172         else:\r
173             p = self.ftpnorm(ftppath)[1:]\r
174             return os.path.normpath(os.path.join(self.root, p))\r
175 \r
176     def fs2ftp(self, fspath):\r
177         """Translate a "real" filesystem pathname into equivalent\r
178         absolute "virtual" ftp pathname depending on the user's\r
179         root directory.\r
180 \r
181         Example (having "/home/user" as root directory):\r
182         >>> fs2ftp("/home/user/foo")\r
183         '/foo'\r
184 \r
185         As for ftpnorm, directory separators are system independent\r
186         ("/") and pathname returned is always absolutized.\r
187 \r
188         On invalid pathnames escaping from user's root directory\r
189         (e.g. "/home" when root is "/home/user") always return "/".\r
190         """\r
191         assert isinstance(fspath, unicode), fspath\r
192         if os.path.isabs(fspath):\r
193             p = os.path.normpath(fspath)\r
194         else:\r
195             p = os.path.normpath(os.path.join(self.root, fspath))\r
196         if not self.validpath(p):\r
197             return u('/')\r
198         p = p.replace(os.sep, "/")\r
199         p = p[len(self.root):]\r
200         if not p.startswith('/'):\r
201             p = '/' + p\r
202         return p\r
203 \r
204     def validpath(self, path):\r
205         """Check whether the path belongs to user's home directory.\r
206         Expected argument is a "real" filesystem pathname.\r
207 \r
208         If path is a symbolic link it is resolved to check its real\r
209         destination.\r
210 \r
211         Pathnames escaping from user's root directory are considered\r
212         not valid.\r
213         """\r
214         assert isinstance(path, unicode), path\r
215         root = self.realpath(self.root)\r
216         path = self.realpath(path)\r
217         if not root.endswith(os.sep):\r
218             root = root + os.sep\r
219         if not path.endswith(os.sep):\r
220             path = path + os.sep\r
221         if path[0:len(root)] == root:\r
222             return True\r
223         return False\r
224 \r
225     # --- Wrapper methods around open() and tempfile.mkstemp\r
226 \r
227     def open(self, filename, mode):\r
228         """Open a file returning its handler."""\r
229         assert isinstance(filename, unicode), filename\r
230         return open(filename, mode)\r
231 \r
232     def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):\r
233         """A wrap around tempfile.mkstemp creating a file with a unique\r
234         name.  Unlike mkstemp it returns an object with a file-like\r
235         interface.\r
236         """\r
237         class FileWrapper:\r
238 \r
239             def __init__(self, fd, name):\r
240                 self.file = fd\r
241                 self.name = name\r
242 \r
243             def __getattr__(self, attr):\r
244                 return getattr(self.file, attr)\r
245 \r
246         text = 'b' not in mode\r
247         # max number of tries to find out a unique file name\r
248         tempfile.TMP_MAX = 50\r
249         fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)\r
250         file = os.fdopen(fd, mode)\r
251         return FileWrapper(file, name)\r
252 \r
253     # --- Wrapper methods around os.* calls\r
254 \r
255     def chdir(self, path):\r
256         """Change the current directory. If this method is overridden\r
257         it is vital that `cwd` attribute gets set.\r
258         """\r
259         # note: process cwd will be reset by the caller\r
260         assert isinstance(path, unicode), path\r
261         os.chdir(path)\r
262         self.cwd = self.fs2ftp(path)\r
263 \r
264     def mkdir(self, path):\r
265         """Create the specified directory."""\r
266         assert isinstance(path, unicode), path\r
267         os.mkdir(path)\r
268 \r
269     def listdir(self, path):\r
270         """List the content of a directory."""\r
271         assert isinstance(path, unicode), path\r
272         return os.listdir(path)\r
273 \r
274     def listdirinfo(self, path):\r
275         """List the content of a directory."""\r
276         assert isinstance(path, unicode), path\r
277         return os.listdir(path)\r
278 \r
279     def rmdir(self, path):\r
280         """Remove the specified directory."""\r
281         assert isinstance(path, unicode), path\r
282         os.rmdir(path)\r
283 \r
284     def remove(self, path):\r
285         """Remove the specified file."""\r
286         assert isinstance(path, unicode), path\r
287         os.remove(path)\r
288 \r
289     def rename(self, src, dst):\r
290         """Rename the specified src file to the dst filename."""\r
291         assert isinstance(src, unicode), src\r
292         assert isinstance(dst, unicode), dst\r
293         os.rename(src, dst)\r
294 \r
295     def chmod(self, path, mode):\r
296         """Change file/directory mode."""\r
297         assert isinstance(path, unicode), path\r
298         if not hasattr(os, 'chmod'):\r
299             raise NotImplementedError\r
300         os.chmod(path, mode)\r
301 \r
302     def stat(self, path):\r
303         """Perform a stat() system call on the given path."""\r
304         # on python 2 we might also get bytes from os.lisdir()\r
305         # assert isinstance(path, unicode), path\r
306         return os.stat(path)\r
307 \r
308     def utime(self, path, timeval):\r
309         """Perform a utime() call on the given path"""\r
310         # utime expects a int/float (atime, mtime) in seconds\r
311         # thus, setting both access and modify time to timeval\r
312         return os.utime(path, (timeval, timeval))\r
313 \r
314     if hasattr(os, 'lstat'):\r
315         def lstat(self, path):\r
316             """Like stat but does not follow symbolic links."""\r
317             # on python 2 we might also get bytes from os.lisdir()\r
318             # assert isinstance(path, unicode), path\r
319             return os.lstat(path)\r
320     else:\r
321         lstat = stat\r
322 \r
323     if hasattr(os, 'readlink'):\r
324         def readlink(self, path):\r
325             """Return a string representing the path to which a\r
326             symbolic link points.\r
327             """\r
328             assert isinstance(path, unicode), path\r
329             return os.readlink(path)\r
330 \r
331     # --- Wrapper methods around os.path.* calls\r
332 \r
333     def isfile(self, path):\r
334         """Return True if path is a file."""\r
335         assert isinstance(path, unicode), path\r
336         return os.path.isfile(path)\r
337 \r
338     def islink(self, path):\r
339         """Return True if path is a symbolic link."""\r
340         assert isinstance(path, unicode), path\r
341         return os.path.islink(path)\r
342 \r
343     def isdir(self, path):\r
344         """Return True if path is a directory."""\r
345         assert isinstance(path, unicode), path\r
346         return os.path.isdir(path)\r
347 \r
348     def getsize(self, path):\r
349         """Return the size of the specified file in bytes."""\r
350         assert isinstance(path, unicode), path\r
351         return os.path.getsize(path)\r
352 \r
353     def getmtime(self, path):\r
354         """Return the last modified time as a number of seconds since\r
355         the epoch."""\r
356         assert isinstance(path, unicode), path\r
357         return os.path.getmtime(path)\r
358 \r
359     def realpath(self, path):\r
360         """Return the canonical version of path eliminating any\r
361         symbolic links encountered in the path (if they are\r
362         supported by the operating system).\r
363         """\r
364         assert isinstance(path, unicode), path\r
365         return os.path.realpath(path)\r
366 \r
367     def lexists(self, path):\r
368         """Return True if path refers to an existing path, including\r
369         a broken or circular symbolic link.\r
370         """\r
371         assert isinstance(path, unicode), path\r
372         return os.path.lexists(path)\r
373 \r
374     if pwd is not None:\r
375         def get_user_by_uid(self, uid):\r
376             """Return the username associated with user id.\r
377             If this can't be determined return raw uid instead.\r
378             On Windows just return "owner".\r
379             """\r
380             try:\r
381                 return pwd.getpwuid(uid).pw_name\r
382             except KeyError:\r
383                 return uid\r
384     else:\r
385         def get_user_by_uid(self, uid):\r
386             return "owner"\r
387 \r
388     if grp is not None:\r
389         def get_group_by_gid(self, gid):\r
390             """Return the groupname associated with group id.\r
391             If this can't be determined return raw gid instead.\r
392             On Windows just return "group".\r
393             """\r
394             try:\r
395                 return grp.getgrgid(gid).gr_name\r
396             except KeyError:\r
397                 return gid\r
398     else:\r
399         def get_group_by_gid(self, gid):\r
400             return "group"\r
401 \r
402     # --- Listing utilities\r
403 \r
404     def format_list(self, basedir, listing, ignore_err=True):\r
405         """Return an iterator object that yields the entries of given\r
406         directory emulating the "/bin/ls -lA" UNIX command output.\r
407 \r
408          - (str) basedir: the absolute dirname.\r
409          - (list) listing: the names of the entries in basedir\r
410          - (bool) ignore_err: when False raise exception if os.lstat()\r
411          call fails.\r
412 \r
413         On platforms which do not support the pwd and grp modules (such\r
414         as Windows), ownership is printed as "owner" and "group" as a\r
415         default, and number of hard links is always "1". On UNIX\r
416         systems, the actual owner, group, and number of links are\r
417         printed.\r
418 \r
419         This is how output appears to client:\r
420 \r
421         -rw-rw-rw-   1 owner   group    7045120 Sep 02  3:47 music.mp3\r
422         drwxrwxrwx   1 owner   group          0 Aug 31 18:50 e-books\r
423         -rw-rw-rw-   1 owner   group        380 Sep 02  3:40 module.py\r
424         """\r
425         @_memoize\r
426         def get_user_by_uid(uid):\r
427             return self.get_user_by_uid(uid)\r
428 \r
429         @_memoize\r
430         def get_group_by_gid(gid):\r
431             return self.get_group_by_gid(gid)\r
432 \r
433         assert isinstance(basedir, unicode), basedir\r
434         if self.cmd_channel.use_gmt_times:\r
435             timefunc = time.gmtime\r
436         else:\r
437             timefunc = time.localtime\r
438         SIX_MONTHS = 180 * 24 * 60 * 60\r
439         readlink = getattr(self, 'readlink', None)\r
440         now = time.time()\r
441         for basename in listing:\r
442             if not PY3:\r
443                 try:\r
444                     file = os.path.join(basedir, basename)\r
445                 except UnicodeDecodeError:\r
446                     # (Python 2 only) might happen on filesystem not\r
447                     # supporting UTF8 meaning os.listdir() returned a list\r
448                     # of mixed bytes and unicode strings:\r
449                     # http://goo.gl/6DLHD\r
450                     # http://bugs.python.org/issue683592\r
451                     file = os.path.join(bytes(basedir), bytes(basename))\r
452                     if not isinstance(basename, unicode):\r
453                         basename = unicode(basename, 'utf8', 'ignore')\r
454             else:\r
455                 file = os.path.join(basedir, basename)\r
456             try:\r
457                 st = self.lstat(file)\r
458             except (OSError, FilesystemError):\r
459                 if ignore_err:\r
460                     continue\r
461                 raise\r
462 \r
463             perms = _filemode(st.st_mode)  # permissions\r
464             nlinks = st.st_nlink  # number of links to inode\r
465             if not nlinks:  # non-posix system, let's use a bogus value\r
466                 nlinks = 1\r
467             size = st.st_size  # file size\r
468             uname = get_user_by_uid(st.st_uid)\r
469             gname = get_group_by_gid(st.st_gid)\r
470             mtime = timefunc(st.st_mtime)\r
471             # if modification time > 6 months shows "month year"\r
472             # else "month hh:mm";  this matches proftpd format, see:\r
473             # https://github.com/giampaolo/pyftpdlib/issues/187\r
474             if (now - st.st_mtime) > SIX_MONTHS:\r
475                 fmtstr = "%d  %Y"\r
476             else:\r
477                 fmtstr = "%d %H:%M"\r
478             try:\r
479                 mtimestr = "%s %s" % (_months_map[mtime.tm_mon],\r
480                                       time.strftime(fmtstr, mtime))\r
481             except ValueError:\r
482                 # It could be raised if last mtime happens to be too\r
483                 # old (prior to year 1900) in which case we return\r
484                 # the current time as last mtime.\r
485                 mtime = timefunc()\r
486                 mtimestr = "%s %s" % (_months_map[mtime.tm_mon],\r
487                                       time.strftime("%d %H:%M", mtime))\r
488 \r
489             # same as stat.S_ISLNK(st.st_mode) but slighlty faster\r
490             islink = (st.st_mode & 61440) == stat.S_IFLNK\r
491             if islink and readlink is not None:\r
492                 # if the file is a symlink, resolve it, e.g.\r
493                 # "symlink -> realfile"\r
494                 try:\r
495                     basename = basename + " -> " + readlink(file)\r
496                 except (OSError, FilesystemError):\r
497                     if not ignore_err:\r
498                         raise\r
499 \r
500             # formatting is matched with proftpd ls output\r
501             line = "%s %3s %-8s %-8s %8s %s %s\r\n" % (\r
502                 perms, nlinks, uname, gname, size, mtimestr, basename)\r
503             yield line.encode('utf8', self.cmd_channel.unicode_errors)\r
504 \r
505     def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):\r
506         """Return an iterator object that yields the entries of a given\r
507         directory or of a single file in a form suitable with MLSD and\r
508         MLST commands.\r
509 \r
510         Every entry includes a list of "facts" referring the listed\r
511         element.  See RFC-3659, chapter 7, to see what every single\r
512         fact stands for.\r
513 \r
514          - (str) basedir: the absolute dirname.\r
515          - (list) listing: the names of the entries in basedir\r
516          - (str) perms: the string referencing the user permissions.\r
517          - (str) facts: the list of "facts" to be returned.\r
518          - (bool) ignore_err: when False raise exception if os.stat()\r
519          call fails.\r
520 \r
521         Note that "facts" returned may change depending on the platform\r
522         and on what user specified by using the OPTS command.\r
523 \r
524         This is how output could appear to the client issuing\r
525         a MLSD request:\r
526 \r
527         type=file;size=156;perm=r;modify=20071029155301;unique=8012; music.mp3\r
528         type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks\r
529         type=file;size=211;perm=r;modify=20071103093626;unique=192; module.py\r
530         """\r
531         assert isinstance(basedir, unicode), basedir\r
532         if self.cmd_channel.use_gmt_times:\r
533             timefunc = time.gmtime\r
534         else:\r
535             timefunc = time.localtime\r
536         permdir = ''.join([x for x in perms if x not in 'arw'])\r
537         permfile = ''.join([x for x in perms if x not in 'celmp'])\r
538         if ('w' in perms) or ('a' in perms) or ('f' in perms):\r
539             permdir += 'c'\r
540         if 'd' in perms:\r
541             permdir += 'p'\r
542         show_type = 'type' in facts\r
543         show_perm = 'perm' in facts\r
544         show_size = 'size' in facts\r
545         show_modify = 'modify' in facts\r
546         show_create = 'create' in facts\r
547         show_mode = 'unix.mode' in facts\r
548         show_uid = 'unix.uid' in facts\r
549         show_gid = 'unix.gid' in facts\r
550         show_unique = 'unique' in facts\r
551         for basename in listing:\r
552             retfacts = dict()\r
553             if not PY3:\r
554                 try:\r
555                     file = os.path.join(basedir, basename)\r
556                 except UnicodeDecodeError:\r
557                     # (Python 2 only) might happen on filesystem not\r
558                     # supporting UTF8 meaning os.listdir() returned a list\r
559                     # of mixed bytes and unicode strings:\r
560                     # http://goo.gl/6DLHD\r
561                     # http://bugs.python.org/issue683592\r
562                     file = os.path.join(bytes(basedir), bytes(basename))\r
563                     if not isinstance(basename, unicode):\r
564                         basename = unicode(basename, 'utf8', 'ignore')\r
565             else:\r
566                 file = os.path.join(basedir, basename)\r
567             # in order to properly implement 'unique' fact (RFC-3659,\r
568             # chapter 7.5.2) we are supposed to follow symlinks, hence\r
569             # use os.stat() instead of os.lstat()\r
570             try:\r
571                 st = self.stat(file)\r
572             except (OSError, FilesystemError):\r
573                 if ignore_err:\r
574                     continue\r
575                 raise\r
576             # type + perm\r
577             # same as stat.S_ISDIR(st.st_mode) but slightly faster\r
578             isdir = (st.st_mode & 61440) == stat.S_IFDIR\r
579             if isdir:\r
580                 if show_type:\r
581                     if basename == '.':\r
582                         retfacts['type'] = 'cdir'\r
583                     elif basename == '..':\r
584                         retfacts['type'] = 'pdir'\r
585                     else:\r
586                         retfacts['type'] = 'dir'\r
587                 if show_perm:\r
588                     retfacts['perm'] = permdir\r
589             else:\r
590                 if show_type:\r
591                     retfacts['type'] = 'file'\r
592                 if show_perm:\r
593                     retfacts['perm'] = permfile\r
594             if show_size:\r
595                 retfacts['size'] = st.st_size  # file size\r
596             # last modification time\r
597             if show_modify:\r
598                 try:\r
599                     retfacts['modify'] = time.strftime("%Y%m%d%H%M%S",\r
600                                                        timefunc(st.st_mtime))\r
601                 # it could be raised if last mtime happens to be too old\r
602                 # (prior to year 1900)\r
603                 except ValueError:\r
604                     pass\r
605             if show_create:\r
606                 # on Windows we can provide also the creation time\r
607                 try:\r
608                     retfacts['create'] = time.strftime("%Y%m%d%H%M%S",\r
609                                                        timefunc(st.st_ctime))\r
610                 except ValueError:\r
611                     pass\r
612             # UNIX only\r
613             if show_mode:\r
614                 retfacts['unix.mode'] = oct(st.st_mode & 511)\r
615             if show_uid:\r
616                 retfacts['unix.uid'] = st.st_uid\r
617             if show_gid:\r
618                 retfacts['unix.gid'] = st.st_gid\r
619 \r
620             # We provide unique fact (see RFC-3659, chapter 7.5.2) on\r
621             # posix platforms only; we get it by mixing st_dev and\r
622             # st_ino values which should be enough for granting an\r
623             # uniqueness for the file listed.\r
624             # The same approach is used by pure-ftpd.\r
625             # Implementors who want to provide unique fact on other\r
626             # platforms should use some platform-specific method (e.g.\r
627             # on Windows NTFS filesystems MTF records could be used).\r
628             if show_unique:\r
629                 retfacts['unique'] = "%xg%x" % (st.st_dev, st.st_ino)\r
630 \r
631             # facts can be in any order but we sort them by name\r
632             factstring = "".join(["%s=%s;" % (x, retfacts[x])\r
633                                   for x in sorted(retfacts.keys())])\r
634             line = "%s %s\r\n" % (factstring, basename)\r
635             yield line.encode('utf8', self.cmd_channel.unicode_errors)\r
636 \r
637 \r
638 # ===================================================================\r
639 # --- platform specific implementation\r
640 # ===================================================================\r
641 \r
642 if os.name == 'posix':\r
643     __all__.append('UnixFilesystem')\r
644 \r
645     class UnixFilesystem(AbstractedFS):\r
646         """Represents the real UNIX filesystem.\r
647 \r
648         Differently from AbstractedFS the client will login into\r
649         /home/<username> and will be able to escape its home directory\r
650         and navigate the real filesystem.\r
651         """\r
652 \r
653         def __init__(self, root, cmd_channel):\r
654             AbstractedFS.__init__(self, root, cmd_channel)\r
655             # initial cwd was set to "/" to emulate a chroot jail\r
656             self.cwd = root\r
657 \r
658         def ftp2fs(self, ftppath):\r
659             return self.ftpnorm(ftppath)\r
660 \r
661         def fs2ftp(self, fspath):\r
662             return fspath\r
663 \r
664         def validpath(self, path):\r
665             # validpath was used to check symlinks escaping user home\r
666             # directory; this is no longer necessary.\r
667             return True\r