1"""
2fs.wrapfs
3=========
4
5A class for wrapping an existing FS object with additional functionality.
6
7This module provides the class WrapFS, a base class for objects that wrap
8another FS object and provide some transformation of its contents.  It could
9be very useful for implementing e.g. transparent encryption or compression
10services.
11
12For a simple example of how this class could be used, see the 'HideDotFilesFS'
13class in the module fs.wrapfs.hidedotfilesfs.  This wrapper implements the
14standard unix shell functionality of hiding dot-files in directory listings.
15
16"""
17
18import re
19import sys
20import fnmatch
21import threading
22
23from fs.base import FS, threading, synchronize, NoDefaultMeta
24from fs.errors import *
25from fs.path import *
26from fs.local_functools import wraps
27
28
29def rewrite_errors(func):
30    """Re-write paths in errors raised by wrapped FS objects."""
31    @wraps(func)
32    def wrapper(self,*args,**kwds):
33        try:
34            return func(self,*args,**kwds)
35        except ResourceError, e:
36            (exc_type,exc_inst,tb) = sys.exc_info()
37            try:
38                e.path = self._decode(e.path)
39            except (AttributeError, ValueError, TypeError):
40                raise e, None, tb
41            raise
42    return wrapper
43
44
45class WrapFS(FS):
46    """FS that wraps another FS, providing translation etc.
47
48    This class allows simple transforms to be applied to the names
49    and/or contents of files in an FS.  It could be used to implement
50    e.g. compression or encryption in a relatively painless manner.
51
52    The following methods can be overridden to control how files are
53    accessed in the underlying FS object:
54
55     * _file_wrap(file, mode):  called for each file that is opened from
56                                the underlying FS; may return a modified
57                                file-like object.
58
59     *  _encode(path):  encode a path for access in the underlying FS
60
61     *  _decode(path):  decode a path from the underlying FS
62
63    If the required path translation proceeds one component at a time,
64    it may be simpler to override the _encode_name() and _decode_name()
65    methods.
66    """
67
68    def __init__(self, fs):
69        super(WrapFS, self).__init__()
70        try:
71            self._lock = fs._lock
72        except (AttributeError,FSError):
73            self._lock = self._lock = threading.RLock()
74        self.wrapped_fs = fs
75
76    def _file_wrap(self, f, mode):
77        """Apply wrapping to an opened file."""
78        return f
79
80    def _encode_name(self, name):
81        """Encode path component for the underlying FS."""
82        return name
83
84    def _decode_name(self, name):
85        """Decode path component from the underlying FS."""
86        return name
87
88    def _encode(self, path):
89        """Encode path for the underlying FS."""
90        e_names = []
91        for name in iteratepath(path):
92            if name == "":
93                e_names.append("")
94            else:
95                e_names.append(self._encode_name(name))
96        return "/".join(e_names)
97
98    def _decode(self, path):
99        """Decode path from the underlying FS."""
100        d_names = []
101        for name in iteratepath(path):
102            if name == "":
103                d_names.append("")
104            else:
105                d_names.append(self._decode_name(name))
106        return "/".join(d_names)
107
108    def _adjust_mode(self, mode):
109        """Adjust the mode used to open a file in the underlying FS.
110
111        This method takes the mode given when opening a file, and should
112        return a two-tuple giving the mode to be used in this FS as first
113        item, and the mode to be used in the underlying FS as the second.
114
115        An example of why this is needed is a WrapFS subclass that does
116        transparent file compression - in this case files from the wrapped
117        FS cannot be opened in append mode.
118        """
119        return (mode, mode)
120
121    def __unicode__(self):
122        return u"<%s: %s>" % (self.__class__.__name__,self.wrapped_fs,)
123
124    #def __str__(self):
125    #    return unicode(self).encode(sys.getdefaultencoding(),"replace")
126
127
128    @rewrite_errors
129    def getmeta(self, meta_name, default=NoDefaultMeta):
130        return self.wrapped_fs.getmeta(meta_name, default)
131
132    @rewrite_errors
133    def hasmeta(self, meta_name):
134        return self.wrapped_fs.hasmeta(meta_name)
135
136    @rewrite_errors
137    def validatepath(self, path):
138        return self.wrapped_fs.validatepath(self._encode(path))
139
140    @rewrite_errors
141    def getsyspath(self, path, allow_none=False):
142        return self.wrapped_fs.getsyspath(self._encode(path), allow_none)
143
144    @rewrite_errors
145    def getpathurl(self, path, allow_none=False):
146        return self.wrapped_fs.getpathurl(self._encode(path), allow_none)
147
148    @rewrite_errors
149    def hassyspath(self, path):
150        return self.wrapped_fs.hassyspath(self._encode(path))
151
152    @rewrite_errors
153    def open(self, path, mode='r', **kwargs):
154        (mode, wmode) = self._adjust_mode(mode)
155        f = self.wrapped_fs.open(self._encode(path), wmode, **kwargs)
156        return self._file_wrap(f, mode)
157
158    @rewrite_errors
159    def setcontents(self, path, data, encoding=None, errors=None, chunk_size=64*1024):
160        #  We can't pass setcontents() through to the wrapped FS if the
161        #  wrapper has defined a _file_wrap method, as it would bypass
162        #  the file contents wrapping.
163        #if self._file_wrap.im_func is WrapFS._file_wrap.im_func:
164        if getattr(self.__class__, '_file_wrap', None) is getattr(WrapFS, '_file_wrap', None):
165            return self.wrapped_fs.setcontents(self._encode(path), data, encoding=encoding, errors=errors, chunk_size=chunk_size)
166        else:
167            return super(WrapFS, self).setcontents(path, data, encoding=encoding, errors=errors, chunk_size=chunk_size)
168
169    @rewrite_errors
170    def createfile(self, path, wipe=False):
171        return self.wrapped_fs.createfile(self._encode(path), wipe=wipe)
172
173    @rewrite_errors
174    def exists(self, path):
175        return self.wrapped_fs.exists(self._encode(path))
176
177    @rewrite_errors
178    def isdir(self, path):
179        return self.wrapped_fs.isdir(self._encode(path))
180
181    @rewrite_errors
182    def isfile(self, path):
183        return self.wrapped_fs.isfile(self._encode(path))
184
185    @rewrite_errors
186    def listdir(self, path="", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
187        kwds = dict(wildcard=wildcard,
188                    full=full,
189                    absolute=absolute,
190                    dirs_only=dirs_only,
191                    files_only=files_only)
192        full = kwds.pop("full",False)
193        absolute = kwds.pop("absolute",False)
194        wildcard = kwds.pop("wildcard",None)
195        if wildcard is None:
196            wildcard = lambda fn:True
197        elif not callable(wildcard):
198            wildcard_re = re.compile(fnmatch.translate(wildcard))
199            wildcard = lambda fn:bool (wildcard_re.match(fn))
200        entries = []
201        enc_path = self._encode(path)
202        for e in self.wrapped_fs.listdir(enc_path,**kwds):
203            e = basename(self._decode(pathcombine(enc_path,e)))
204            if not wildcard(e):
205                continue
206            if full:
207                e = pathcombine(path,e)
208            elif absolute:
209                e = abspath(pathcombine(path,e))
210            entries.append(e)
211        return entries
212
213    @rewrite_errors
214    def ilistdir(self, path="", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
215        kwds = dict(wildcard=wildcard,
216                    full=full,
217                    absolute=absolute,
218                    dirs_only=dirs_only,
219                    files_only=files_only)
220        full = kwds.pop("full",False)
221        absolute = kwds.pop("absolute",False)
222        wildcard = kwds.pop("wildcard",None)
223        if wildcard is None:
224            wildcard = lambda fn:True
225        elif not callable(wildcard):
226            wildcard_re = re.compile(fnmatch.translate(wildcard))
227            wildcard = lambda fn:bool (wildcard_re.match(fn))
228        enc_path = self._encode(path)
229        for e in self.wrapped_fs.ilistdir(enc_path,**kwds):
230            e = basename(self._decode(pathcombine(enc_path,e)))
231            if not wildcard(e):
232                continue
233            if full:
234                e = pathcombine(path,e)
235            elif absolute:
236                e = abspath(pathcombine(path,e))
237            yield e
238
239    @rewrite_errors
240    def listdirinfo(self, path="", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
241        kwds = dict(wildcard=wildcard,
242                    full=full,
243                    absolute=absolute,
244                    dirs_only=dirs_only,
245                    files_only=files_only)
246        full = kwds.pop("full",False)
247        absolute = kwds.pop("absolute",False)
248        wildcard = kwds.pop("wildcard",None)
249        if wildcard is None:
250            wildcard = lambda fn:True
251        elif not callable(wildcard):
252            wildcard_re = re.compile(fnmatch.translate(wildcard))
253            wildcard = lambda fn:bool (wildcard_re.match(fn))
254        entries = []
255        enc_path = self._encode(path)
256        for (nm,info) in self.wrapped_fs.listdirinfo(enc_path,**kwds):
257            nm = basename(self._decode(pathcombine(enc_path,nm)))
258            if not wildcard(nm):
259                continue
260            if full:
261                nm = pathcombine(path,nm)
262            elif absolute:
263                nm = abspath(pathcombine(path,nm))
264            entries.append((nm,info))
265        return entries
266
267    @rewrite_errors
268    def ilistdirinfo(self, path="", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
269        kwds = dict(wildcard=wildcard,
270                    full=full,
271                    absolute=absolute,
272                    dirs_only=dirs_only,
273                    files_only=files_only)
274        full = kwds.pop("full",False)
275        absolute = kwds.pop("absolute",False)
276        wildcard = kwds.pop("wildcard",None)
277        if wildcard is None:
278            wildcard = lambda fn:True
279        elif not callable(wildcard):
280            wildcard_re = re.compile(fnmatch.translate(wildcard))
281            wildcard = lambda fn:bool (wildcard_re.match(fn))
282        enc_path = self._encode(path)
283        for (nm,info) in self.wrapped_fs.ilistdirinfo(enc_path,**kwds):
284            nm = basename(self._decode(pathcombine(enc_path,nm)))
285            if not wildcard(nm):
286                continue
287            if full:
288                nm = pathcombine(path,nm)
289            elif absolute:
290                nm = abspath(pathcombine(path,nm))
291            yield (nm,info)
292
293    @rewrite_errors
294    def walk(self,path="/",wildcard=None,dir_wildcard=None,search="breadth",ignore_errors=False):
295        if dir_wildcard is not None:
296            #  If there is a dir_wildcard, fall back to the default impl
297            #  that uses listdir().  Otherwise we run the risk of enumerating
298            #  lots of directories that will just be thrown away.
299            for item in super(WrapFS,self).walk(path,wildcard,dir_wildcard,search,ignore_errors):
300                yield item
301        #  Otherwise, the wrapped FS may provide a more efficient impl
302        #  which we can use directly.
303        else:
304            if wildcard is not None and not callable(wildcard):
305                wildcard_re = re.compile(fnmatch.translate(wildcard))
306                wildcard = lambda fn:bool (wildcard_re.match(fn))
307            for (dirpath,filepaths) in self.wrapped_fs.walk(self._encode(path),search=search,ignore_errors=ignore_errors):
308                filepaths = [basename(self._decode(pathcombine(dirpath,p)))
309                                 for p in filepaths]
310                dirpath = abspath(self._decode(dirpath))
311                if wildcard is not None:
312                    filepaths = [p for p in filepaths if wildcard(p)]
313                yield (dirpath,filepaths)
314
315    @rewrite_errors
316    def walkfiles(self,path="/",wildcard=None,dir_wildcard=None,search="breadth",ignore_errors=False):
317        if dir_wildcard is not None:
318            #  If there is a dir_wildcard, fall back to the default impl
319            #  that uses listdir().  Otherwise we run the risk of enumerating
320            #  lots of directories that will just be thrown away.
321            for item in super(WrapFS,self).walkfiles(path,wildcard,dir_wildcard,search,ignore_errors):
322                yield item
323        #  Otherwise, the wrapped FS may provide a more efficient impl
324        #  which we can use directly.
325        else:
326            if wildcard is not None and not callable(wildcard):
327                wildcard_re = re.compile(fnmatch.translate(wildcard))
328                wildcard = lambda fn:bool (wildcard_re.match(fn))
329            for filepath in self.wrapped_fs.walkfiles(self._encode(path),search=search,ignore_errors=ignore_errors):
330                filepath = abspath(self._decode(filepath))
331                if wildcard is not None:
332                    if not wildcard(basename(filepath)):
333                        continue
334                yield filepath
335
336    @rewrite_errors
337    def walkdirs(self,path="/",wildcard=None,search="breadth",ignore_errors=False):
338        if wildcard is not None:
339            #  If there is a wildcard, fall back to the default impl
340            #  that uses listdir().  Otherwise we run the risk of enumerating
341            #  lots of directories that will just be thrown away.
342            for item in super(WrapFS,self).walkdirs(path,wildcard,search,ignore_errors):
343                yield item
344        #  Otherwise, the wrapped FS may provide a more efficient impl
345        #  which we can use directly.
346        else:
347            for dirpath in self.wrapped_fs.walkdirs(self._encode(path),search=search,ignore_errors=ignore_errors):
348                yield abspath(self._decode(dirpath))
349
350
351    @rewrite_errors
352    def makedir(self, path, *args, **kwds):
353        return self.wrapped_fs.makedir(self._encode(path),*args,**kwds)
354
355    @rewrite_errors
356    def remove(self, path):
357        return self.wrapped_fs.remove(self._encode(path))
358
359    @rewrite_errors
360    def removedir(self, path, *args, **kwds):
361        return self.wrapped_fs.removedir(self._encode(path),*args,**kwds)
362
363    @rewrite_errors
364    def rename(self, src, dst):
365        return self.wrapped_fs.rename(self._encode(src),self._encode(dst))
366
367    @rewrite_errors
368    def getinfo(self, path):
369        return self.wrapped_fs.getinfo(self._encode(path))
370
371    @rewrite_errors
372    def settimes(self, path, *args, **kwds):
373        return self.wrapped_fs.settimes(self._encode(path), *args,**kwds)
374
375    @rewrite_errors
376    def desc(self, path):
377        return self.wrapped_fs.desc(self._encode(path))
378
379    @rewrite_errors
380    def copy(self, src, dst, **kwds):
381        return self.wrapped_fs.copy(self._encode(src),self._encode(dst),**kwds)
382
383    @rewrite_errors
384    def move(self, src, dst, **kwds):
385        return self.wrapped_fs.move(self._encode(src),self._encode(dst),**kwds)
386
387    @rewrite_errors
388    def movedir(self, src, dst, **kwds):
389        return self.wrapped_fs.movedir(self._encode(src),self._encode(dst),**kwds)
390
391    @rewrite_errors
392    def copydir(self, src, dst, **kwds):
393        return self.wrapped_fs.copydir(self._encode(src),self._encode(dst),**kwds)
394
395    @rewrite_errors
396    def getxattr(self, path, name, default=None):
397        try:
398            return self.wrapped_fs.getxattr(self._encode(path),name,default)
399        except AttributeError:
400            raise UnsupportedError("getxattr")
401
402    @rewrite_errors
403    def setxattr(self, path, name, value):
404        try:
405            return self.wrapped_fs.setxattr(self._encode(path),name,value)
406        except AttributeError:
407            raise UnsupportedError("setxattr")
408
409    @rewrite_errors
410    def delxattr(self, path, name):
411        try:
412            return self.wrapped_fs.delxattr(self._encode(path),name)
413        except AttributeError:
414            raise UnsupportedError("delxattr")
415
416    @rewrite_errors
417    def listxattrs(self, path):
418        try:
419            return self.wrapped_fs.listxattrs(self._encode(path))
420        except AttributeError:
421            raise UnsupportedError("listxattrs")
422
423    def __getattr__(self, attr):
424        #  These attributes can be used by the destructor, but may not be
425        #  defined if there are errors in the constructor.
426        if attr == "closed":
427            return False
428        if attr == "wrapped_fs":
429            return None
430        if attr.startswith("_"):
431            raise AttributeError(attr)
432        return getattr(self.wrapped_fs,attr)
433
434    @rewrite_errors
435    def close(self):
436        if not self.closed:
437            self.wrapped_fs.close()
438            super(WrapFS,self).close()
439            self.wrapped_fs = None
440
441
442def wrap_fs_methods(decorator, cls=None, exclude=[]):
443    """Apply the given decorator to all FS methods on the given class.
444
445    This function can be used in two ways.  When called with two arguments it
446    applies the given function 'decorator' to each FS method of the given
447    class.  When called with just a single argument, it creates and returns
448    a class decorator which will do the same thing when applied.  So you can
449    use it like this::
450
451        wrap_fs_methods(mydecorator,MyFSClass)
452
453    Or on more recent Python versions, like this::
454
455        @wrap_fs_methods(mydecorator)
456        class MyFSClass(FS):
457            ...
458
459    """
460    def apply_decorator(cls):
461        for method_name in wrap_fs_methods.method_names:
462            if method_name in exclude:
463                continue
464            method = getattr(cls,method_name,None)
465            if method is not None:
466                setattr(cls,method_name,decorator(method))
467        return cls
468    if cls is not None:
469        return apply_decorator(cls)
470    else:
471        return apply_decorator
472
473wrap_fs_methods.method_names = ["open","exists","isdir","isfile","listdir",
474    "makedir","remove","setcontents","removedir","rename","getinfo","copy",
475    "move","copydir","movedir","close","getxattr","setxattr","delxattr",
476    "listxattrs","validatepath","getsyspath","createfile", "hasmeta", "getmeta","listdirinfo",
477    "ilistdir","ilistdirinfo"]
478
479
480