filesystems.py 25 KB

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