1"""
2fs.mountfs
3==========
4
5Contains MountFS class which is a virtual filesystem which can have other filesystems linked as branched directories.
6
7For example, lets say we have two filesystems containing config files and resources respectively::
8
9   [config_fs]
10   |-- config.cfg
11   `-- defaults.cfg
12
13   [resources_fs]
14   |-- images
15   |   |-- logo.jpg
16   |   `-- photo.jpg
17   `-- data.dat
18
19We can combine these filesystems in to a single filesystem with the following code::
20
21    from fs.mountfs import MountFS
22    combined_fs = MountFS()
23    combined_fs.mountdir('config', config_fs)
24    combined_fs.mountdir('resources', resources_fs)
25
26This will create a single filesystem where paths under `config` map to `config_fs`, and paths under `resources` map to `resources_fs`::
27
28    [combined_fs]
29    |-- config
30    |   |-- config.cfg
31    |   `-- defaults.cfg
32    `-- resources
33        |-- images
34        |   |-- logo.jpg
35        |   `-- photo.jpg
36        `-- data.dat
37
38Now both filesystems can be accessed with the same path structure::
39
40    print combined_fs.getcontents('/config/defaults.cfg')
41    read_jpg(combined_fs.open('/resources/images/logo.jpg')
42
43"""
44
45from fs.base import *
46from fs.errors import *
47from fs.path import *
48from fs import _thread_synchronize_default
49from fs import iotools
50
51
52class DirMount(object):
53    def __init__(self, path, fs):
54        self.path = path
55        self.fs = fs
56
57    def __str__(self):
58        return "<DirMount %s, %s>" % (self.path, self.fs)
59
60    def __repr__(self):
61        return "<DirMount %s, %s>" % (self.path, self.fs)
62
63    def __unicode__(self):
64        return u"<DirMount %s, %s>" % (self.path, self.fs)
65
66
67class FileMount(object):
68    def __init__(self, path, open_callable, info_callable=None):
69        self.open_callable = open_callable
70        def no_info_callable(path):
71            return {}
72        self.info_callable = info_callable or no_info_callable
73
74
75class MountFS(FS):
76    """A filesystem that delegates to other filesystems."""
77
78    _meta = { 'virtual': True,
79              'read_only' : False,
80              'unicode_paths' : True,
81              'case_insensitive_paths' : False,
82              }
83
84    DirMount = DirMount
85    FileMount = FileMount
86
87    def __init__(self, auto_close=True, thread_synchronize=_thread_synchronize_default):
88        self.auto_close = auto_close
89        super(MountFS, self).__init__(thread_synchronize=thread_synchronize)
90        self.mount_tree = PathMap()
91
92    def __str__(self):
93        return "<%s [%s]>" % (self.__class__.__name__,self.mount_tree.items(),)
94
95    __repr__ = __str__
96
97    def __unicode__(self):
98        return u"<%s [%s]>" % (self.__class__.__name__,self.mount_tree.items(),)
99
100    def _delegate(self, path):
101        path = abspath(normpath(path))
102        object = None
103        head_path = "/"
104        tail_path = path
105
106        for prefix in recursepath(path):
107            try:
108                object = self.mount_tree[prefix]
109            except KeyError:
110                pass
111            else:
112                head_path = prefix
113                tail_path = path[len(head_path):]
114
115        if type(object) is MountFS.DirMount:
116            return object.fs, head_path, tail_path
117
118        if type(object) is MountFS.FileMount:
119            return self, "/", path
120
121        try:
122            self.mount_tree.iternames(path).next()
123        except StopIteration:
124            return None, None, None
125        else:
126            return self, "/", path
127
128    @synchronize
129    def close(self):
130        # Explicitly closes children if requested
131        if self.auto_close:
132            for mount in self.mount_tree.itervalues():
133                mount.fs.close()
134        # Free references (which may incidently call the close method of the child filesystems)
135        self.mount_tree.clear()
136        super(MountFS, self).close()
137
138    def getsyspath(self, path, allow_none=False):
139        fs, _mount_path, delegate_path = self._delegate(path)
140        if fs is self or fs is None:
141            if allow_none:
142                return None
143            else:
144                raise NoSysPathError(path=path)
145        return fs.getsyspath(delegate_path, allow_none=allow_none)
146
147    def getpathurl(self, path, allow_none=False):
148        fs, _mount_path, delegate_path = self._delegate(path)
149        if fs is self or fs is None:
150            if allow_none:
151                return None
152            else:
153                raise NoPathURLError(path=path)
154        return fs.getpathurl(delegate_path, allow_none=allow_none)
155
156    @synchronize
157    def desc(self, path):
158        fs, _mount_path, delegate_path = self._delegate(path)
159        if fs is self:
160            if fs.isdir(path):
161                return "Mount dir"
162            else:
163                return "Mounted file"
164        return "Mounted dir, maps to path %s on %s" % (abspath(delegate_path) or '/', str(fs))
165
166    @synchronize
167    def isdir(self, path):
168        fs, _mount_path, delegate_path = self._delegate(path)
169        if fs is None:
170            path = normpath(path)
171            if path in ("/", ""):
172                return True
173            return False
174        if fs is self:
175            obj = self.mount_tree.get(path, None)
176            return not isinstance(obj, MountFS.FileMount)
177        return fs.isdir(delegate_path)
178
179    @synchronize
180    def isfile(self, path):
181        fs, _mount_path, delegate_path = self._delegate(path)
182        if fs is None:
183            return False
184        if fs is self:
185            obj = self.mount_tree.get(path, None)
186            return isinstance(obj, MountFS.FileMount)
187        return fs.isfile(delegate_path)
188
189    @synchronize
190    def exists(self, path):
191        if path in ("/", ""):
192            return True
193        fs, _mount_path, delegate_path = self._delegate(path)
194        if fs is None:
195            return False
196        if fs is self:
197            return True
198        return fs.exists(delegate_path)
199
200    @synchronize
201    def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
202        fs, _mount_path, delegate_path = self._delegate(path)
203
204        if fs is None:
205            if path in ("/", ""):
206                return []
207            raise ResourceNotFoundError("path")
208
209        elif fs is self:
210            paths = self.mount_tree.names(path)
211            return self._listdir_helper(path,
212                                        paths,
213                                        wildcard,
214                                        full,
215                                        absolute,
216                                        dirs_only,
217                                        files_only)
218        else:
219            paths = fs.listdir(delegate_path,
220                               wildcard=wildcard,
221                               full=False,
222                               absolute=False,
223                               dirs_only=dirs_only,
224                               files_only=files_only)
225            for nm in self.mount_tree.names(path):
226                if nm not in paths:
227                    if dirs_only:
228                        if self.isdir(pathjoin(path,nm)):
229                            paths.append(nm)
230                    elif files_only:
231                        if self.isfile(pathjoin(path,nm)):
232                            paths.append(nm)
233                    else:
234                        paths.append(nm)
235            if full or absolute:
236                if full:
237                    path = relpath(normpath(path))
238                else:
239                    path = abspath(normpath(path))
240                paths = [pathjoin(path, p) for p in paths]
241
242            return paths
243
244    @synchronize
245    def ilistdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False):
246        fs, _mount_path, delegate_path = self._delegate(path)
247
248        if fs is None:
249            if path in ("/", ""):
250                return
251            raise ResourceNotFoundError(path)
252
253        if fs is self:
254            paths = self.mount_tree.names(path)
255            for path in self._listdir_helper(path,paths,wildcard,full,absolute,dirs_only,files_only):
256                yield path
257        else:
258            paths = fs.ilistdir(delegate_path,
259                                wildcard=wildcard,
260                                full=False,
261                                absolute=False,
262                                dirs_only=dirs_only)
263            extra_paths = set(self.mount_tree.names(path))
264            if full:
265                pathhead = relpath(normpath(path))
266                def mkpath(p):
267                    return pathjoin(pathhead,p)
268            elif absolute:
269                pathhead = abspath(normpath(path))
270                def mkpath(p):
271                    return pathjoin(pathhead,p)
272            else:
273                def mkpath(p):
274                    return p
275            for p in paths:
276                if p not in extra_paths:
277                    yield mkpath(p)
278            for p in extra_paths:
279                if dirs_only:
280                    if self.isdir(pathjoin(path,p)):
281                        yield mkpath(p)
282                elif files_only:
283                    if self.isfile(pathjoin(path,p)):
284                        yield mkpath(p)
285                else:
286                    yield mkpath(p)
287
288    @synchronize
289    def makedir(self, path, recursive=False, allow_recreate=False):
290        fs, _mount_path, delegate_path = self._delegate(path)
291        if fs is self or fs is None:
292            raise UnsupportedError("make directory", msg="Can only makedir for mounted paths")
293        if not delegate_path:
294            if allow_recreate:
295                return
296            else:
297                raise DestinationExistsError(path, msg="Can not create a directory that already exists (try allow_recreate=True): %(path)s")
298        return fs.makedir(delegate_path, recursive=recursive, allow_recreate=allow_recreate)
299
300    @synchronize
301    def open(self, path, mode='r', buffering=-1, encoding=None, errors=None, newline=None, line_buffering=False, **kwargs):
302        obj = self.mount_tree.get(path, None)
303        if type(obj) is MountFS.FileMount:
304            callable = obj.open_callable
305            return callable(path, mode, **kwargs)
306
307        fs, _mount_path, delegate_path = self._delegate(path)
308
309        if fs is self or fs is None:
310            raise ResourceNotFoundError(path)
311
312        return fs.open(delegate_path, mode, **kwargs)
313
314    @synchronize
315    def setcontents(self, path, data=b'', encoding=None, errors=None, chunk_size=64*1024):
316        obj = self.mount_tree.get(path, None)
317        if type(obj) is MountFS.FileMount:
318            return super(MountFS, self).setcontents(path,
319                                                    data,
320                                                    encoding=encoding,
321                                                    errors=errors,
322                                                    chunk_size=chunk_size)
323        fs, _mount_path, delegate_path = self._delegate(path)
324        if fs is self or fs is None:
325            raise ParentDirectoryMissingError(path)
326        return fs.setcontents(delegate_path, data, encoding=encoding, errors=errors, chunk_size=chunk_size)
327
328    @synchronize
329    def createfile(self, path, wipe=False):
330        obj = self.mount_tree.get(path, None)
331        if type(obj) is MountFS.FileMount:
332            return super(MountFS, self).createfile(path, wipe=wipe)
333        fs, _mount_path, delegate_path = self._delegate(path)
334        if fs is self or fs is None:
335            raise ParentDirectoryMissingError(path)
336        return fs.createfile(delegate_path, wipe=wipe)
337
338    @synchronize
339    def remove(self, path):
340        fs, _mount_path, delegate_path = self._delegate(path)
341        if fs is self or fs is None:
342            raise UnsupportedError("remove file", msg="Can only remove paths within a mounted dir")
343        return fs.remove(delegate_path)
344
345    @synchronize
346    def removedir(self, path, recursive=False, force=False):
347        path = normpath(path)
348        if path in ('', '/'):
349            raise RemoveRootError(path)
350        fs, _mount_path, delegate_path = self._delegate(path)
351        if fs is self or fs is None:
352            raise ResourceInvalidError(path, msg="Can not removedir for an un-mounted path")
353        return fs.removedir(delegate_path, recursive, force)
354
355    @synchronize
356    def rename(self, src, dst):
357        fs1, _mount_path1, delegate_path1 = self._delegate(src)
358        fs2, _mount_path2, delegate_path2 = self._delegate(dst)
359
360        if fs1 is not fs2:
361            raise OperationFailedError("rename resource", path=src)
362
363        if fs1 is not self:
364            return fs1.rename(delegate_path1, delegate_path2)
365
366        object = self.mount_tree.get(src, None)
367        _object2 = self.mount_tree.get(dst, None)
368
369        if object is None:
370            raise ResourceNotFoundError(src)
371
372        raise UnsupportedError("rename resource", path=src)
373
374    @synchronize
375    def move(self,src,dst,**kwds):
376        fs1, _mount_path1, delegate_path1 = self._delegate(src)
377        fs2, _mount_path2, delegate_path2 = self._delegate(dst)
378        if fs1 is fs2 and fs1 is not self:
379            fs1.move(delegate_path1,delegate_path2,**kwds)
380        else:
381            super(MountFS,self).move(src,dst,**kwds)
382
383    @synchronize
384    def movedir(self,src,dst,**kwds):
385        fs1, _mount_path1, delegate_path1 = self._delegate(src)
386        fs2, _mount_path2, delegate_path2 = self._delegate(dst)
387        if fs1 is fs2 and fs1 is not self:
388            fs1.movedir(delegate_path1,delegate_path2,**kwds)
389        else:
390            super(MountFS,self).movedir(src,dst,**kwds)
391
392    @synchronize
393    def copy(self,src,dst,**kwds):
394        fs1, _mount_path1, delegate_path1 = self._delegate(src)
395        fs2, _mount_path2, delegate_path2 = self._delegate(dst)
396        if fs1 is fs2 and fs1 is not self:
397            fs1.copy(delegate_path1,delegate_path2,**kwds)
398        else:
399            super(MountFS,self).copy(src,dst,**kwds)
400
401    @synchronize
402    def copydir(self,src,dst,**kwds):
403        fs1, _mount_path1, delegate_path1 = self._delegate(src)
404        fs2, _mount_path2, delegate_path2 = self._delegate(dst)
405        if fs1 is fs2 and fs1 is not self:
406            fs1.copydir(delegate_path1,delegate_path2,**kwds)
407        else:
408            super(MountFS,self).copydir(src,dst,**kwds)
409
410    @synchronize
411    def mountdir(self, path, fs):
412        """Mounts a host FS object on a given path.
413
414        :param path: A path within the MountFS
415        :param fs: A filesystem object to mount
416
417        """
418        path = abspath(normpath(path))
419        self.mount_tree[path] = MountFS.DirMount(path, fs)
420    mount = mountdir
421
422    @synchronize
423    def mountfile(self, path, open_callable=None, info_callable=None):
424        """Mounts a single file path.
425
426        :param path: A path within the MountFS
427        :param open_callable: A callable that returns a file-like object,
428            `open_callable` should have the same signature as :py:meth:`~fs.base.FS.open`
429        :param info_callable: A callable that returns a dictionary with information regarding the file-like object,
430            `info_callable` should have the same signagture as :py:meth:`~fs.base.FS.getinfo`
431
432        """
433        self.mount_tree[path] = MountFS.FileMount(path, open_callable, info_callable)
434
435    @synchronize
436    def unmount(self, path):
437        """Unmounts a path.
438
439        :param path: Path to unmount
440        :return: True if a path was unmounted, False if the path was already unmounted
441        :rtype: bool
442
443        """
444        try:
445            del self.mount_tree[path]
446        except KeyError:
447            return False
448        else:
449            return True
450
451    @synchronize
452    def settimes(self, path, accessed_time=None, modified_time=None):
453        path = normpath(path)
454        fs, _mount_path, delegate_path = self._delegate(path)
455
456        if fs is None:
457            raise ResourceNotFoundError(path)
458
459        if fs is self:
460            raise UnsupportedError("settimes")
461        fs.settimes(delegate_path, accessed_time, modified_time)
462
463    @synchronize
464    def getinfo(self, path):
465        path = normpath(path)
466
467        fs, _mount_path, delegate_path = self._delegate(path)
468
469        if fs is None:
470            if path in ("/", ""):
471                return {}
472            raise ResourceNotFoundError(path)
473
474        if fs is self:
475            if self.isfile(path):
476                return self.mount_tree[path].info_callable(path)
477            return {}
478        return fs.getinfo(delegate_path)
479
480    @synchronize
481    def getsize(self, path):
482        path = normpath(path)
483        fs, _mount_path, delegate_path = self._delegate(path)
484
485        if fs is None:
486            raise ResourceNotFoundError(path)
487
488        if fs is self:
489            object = self.mount_tree.get(path, None)
490
491            if object is None:
492                raise ResourceNotFoundError(path)
493            if not isinstance(object,MountFS.FileMount):
494                raise ResourceInvalidError(path)
495
496            size = object.info_callable(path).get("size", None)
497            return size
498
499        return fs.getinfo(delegate_path).get("size", None)
500
501    @synchronize
502    def getxattr(self,path,name,default=None):
503        path = normpath(path)
504        fs, _mount_path, delegate_path = self._delegate(path)
505        if fs is None:
506            if path in ("/", ""):
507                return default
508            raise ResourceNotFoundError(path)
509        if fs is self:
510            return default
511        return fs.getxattr(delegate_path,name,default)
512
513    @synchronize
514    def setxattr(self,path,name,value):
515        path = normpath(path)
516        fs, _mount_path, delegate_path = self._delegate(path)
517        if fs is None:
518            raise ResourceNotFoundError(path)
519        if fs is self:
520            raise UnsupportedError("setxattr")
521        return fs.setxattr(delegate_path,name,value)
522
523    @synchronize
524    def delxattr(self,path,name):
525        path = normpath(path)
526        fs, _mount_path, delegate_path = self._delegate(path)
527        if fs is None:
528            raise ResourceNotFoundError(path)
529        if fs is self:
530            return True
531        return fs.delxattr(delegate_path, name)
532
533    @synchronize
534    def listxattrs(self,path):
535        path = normpath(path)
536        fs, _mount_path, delegate_path = self._delegate(path)
537        if fs is None:
538            raise ResourceNotFoundError(path)
539        if fs is self:
540            return []
541        return fs.listxattrs(delegate_path)
542
543
544