summaryrefslogtreecommitdiff
path: root/fs/expose/fuse/__init__.py
blob: 156aa969c633867b1903b321115c2239a0534e11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
"""

  fs.expose.fuse:  expose an FS object to the native filesystem via FUSE

This module provides the necessay interfaces to mount an FS object into
the local filesystem via FUSE:

    http://fuse.sourceforge.net/

For simple usage, the function 'mount' takes an FS object and a local path,
and exposes the given FS at that path:

    >>> from fs.memoryfs import MemoryFS
    >>> from fs.expose import fuse
    >>> fs = MemoryFS()
    >>> mp = fuse.mount(fs,"/mnt/my-memory-fs")
    >>> mp.path
    '/mnt/my-memory-fs'
    >>> mp.unmount()

The above spawns a new background process to manage the FUSE event loop, which
can be controlled through the returned subprocess.Popen object.  To avoid
spawning a new process, set the 'foreground' option:

    >>> #  This will block until the filesystem is unmounted
    >>> fuse.mount(fs,"/mnt/my-memory-fs",foreground=True)

Any additional options for the FUSE process can be passed as keyword arguments
to the 'mount' function.

If you require finer control over the creation of the FUSE process, you can
instantiate the MountProcess class directly.  It accepts all options available
to subprocess.Popen:

    >>> from subprocess import PIPE
    >>> mp = fuse.MountProcess(fs,"/mnt/my-memory-fs",stderr=PIPE)
    >>> fuse_errors = mp.communicate()[1]

The binding to FUSE is created via ctypes, using a custom version of the
fuse.py code from Giorgos Verigakis:

    http://code.google.com/p/fusepy/

"""

import os
import sys
import signal
import errno
import time
import stat as statinfo
import subprocess
import pickle

from fs.base import flags_to_mode
from fs.errors import *
from fs.path import *
from fs.xattrs import ensure_xattrs

import fuse_ctypes as fuse
try:
    fuse._libfuse.fuse_get_context
except AttributeError:
    raise ImportError("could not locate FUSE library")


FUSE = fuse.FUSE
Operations = fuse.Operations
fuse_get_context = fuse.fuse_get_context

STARTUP_TIME = time.time()


def handle_fs_errors(func):
    """Method decorator to report FS errors in the appropriate way.

    This decorator catches all FS errors and translates them into an
    equivalent OSError.  It also makes the function return zero instead
    of None as an indication of successful execution.
    """
    def wrapper(*args,**kwds):
        try:
            res = func(*args,**kwds)
        except ResourceNotFoundError, e:
            raise OSError(errno.ENOENT,str(e))
        except FSError, e:
            raise OSError(errno.EFAULT,str(e))
        except Exception, e:
            raise
        if res is None:
            return 0
        return res
    return wrapper
 

def get_stat_dict(fs,path):
    """Build a 'stat' dictionary for the given file."""
    uid, gid, pid = fuse_get_context()
    info = fs.getinfo(path)
    private_keys = [k for k in info if k.startswith("_")]
    for k in private_keys:
        del info[k]
    #  Basic stuff that is constant for all paths
    info.setdefault("st_ino",0)
    info.setdefault("st_dev",0)
    info.setdefault("st_uid",uid)
    info.setdefault("st_gid",gid)
    info.setdefault("st_rdev",0)
    info.setdefault("st_blksize",1024)
    info.setdefault("st_blocks",1)
    #  The interesting stuff
    info.setdefault("st_size",info.get("size",1024))
    info.setdefault("st_mode",info.get('st_mode',0700))
    if fs.isdir(path):
        info["st_mode"] = info["st_mode"] | statinfo.S_IFDIR
        info.setdefault("st_nlink",2)
    else:
        info["st_mode"] = info["st_mode"] | statinfo.S_IFREG
        info.setdefault("st_nlink",1)
    for (key1,key2) in [("st_atime","accessed_time"),("st_mtime","modified_time"),("st_ctime","created_time")]:
        if key1 not in info:
            if key2 in info:
                info[key1] = time.mktime(info[key2].timetuple())
            else:
                info[key1] = STARTUP_TIME
    return info
 

class FSOperations(Operations):
    """FUSE Operations interface delegating all activities to an FS object."""

    def __init__(self,fs,on_init=None,on_destroy=None):
        self.fs = ensure_xattrs(fs)
        self._fhmap = {}
        self._on_init = on_init
        self._on_destroy = on_destroy

    def _get_file(self,fh):
        try:
            return self._fhmap[fh]
        except KeyError:
            raise FSError("invalid file handle")

    def _reg_file(self,f):
        # TODO: a better handle-generation routine
        fh = int(time.time()*1000)
        self._fhmap.setdefault(fh,f)
        if self._fhmap[fh] is not f:
            return self._reg_file(f)
        return fh

    def init(self,conn):
        if self._on_init:
            self._on_init()

    def destroy(self,data):
        if self._on_destroy:
            self._on_destroy()
    
    @handle_fs_errors
    def chmod(self,path,mode):
        raise UnsupportedError("chmod")
    
    @handle_fs_errors
    def chown(self,path,uid,gid):
        raise UnsupportedError("chown")

    @handle_fs_errors
    def create(self,path,mode,fi=None):
        if fi is not None:
            raise UnsupportedError("raw_fi")
        return self._reg_file(self.fs.open(path,"w"))

    @handle_fs_errors
    def flush(self,path,fh):
        self._get_file(fh).flush()

    @handle_fs_errors
    def getattr(self,path,fh=None):
        return get_stat_dict(self.fs,path)

    @handle_fs_errors
    def getxattr(self,path,name,position=0):
        try:
            value = self.fs.getxattr(path,name)
        except AttributeError:
            raise UnsupportedError("getxattr")
        else:
            if value is None:
                raise OSError(errno.ENOENT,"no attribute '%s'" % (name,))
            return value

    @handle_fs_errors
    def link(self,target,souce):
        raise UnsupportedError("link")

    @handle_fs_errors
    def listxattr(self,path):
        try:
            return self.fs.listxattrs(path)
        except AttributeError:
            raise UnsupportedError("listxattr")

    @handle_fs_errors
    def mkdir(self,path,mode):
        try:
            self.fs.makedir(path,mode)
        except TypeError:
            self.fs.makedir(path)

    @handle_fs_errors
    def mknod(self,path,mode,dev):
        raise UnsupportedError("mknod")

    @handle_fs_errors
    def open(self,path,flags):
        mode = flags_to_mode(flags)
        return self._reg_file(self.fs.open(path,mode))

    @handle_fs_errors
    def read(self,path,size,offset,fh):
        f = self._get_file(fh)
        f.seek(offset)
        return f.read(size)

    @handle_fs_errors
    def readdir(self,path,fh=None):
        return ['.', '..'] + self.fs.listdir(path)

    @handle_fs_errors
    def readlink(self,path):
        raise UnsupportedError("readlink")

    @handle_fs_errors
    def release(self,path,fh):
        self._get_file(fh).close()
        del self._fhmap[fh]

    @handle_fs_errors
    def removexattr(self,path,name):
        try:
            return self.fs.delxattr(path,name)
        except AttributeError:
            raise UnsupportedError("removexattr")

    @handle_fs_errors
    def rename(self,old,new):
        if issamedir(old,new):
            self.fs.rename(old,new)
        else:
            if self.fs.isdir(old):
                self.fs.movedir(old,new)
            else:
                self.fs.move(old,new)

    @handle_fs_errors
    def rmdir(self, path):
        self.fs.removedir(path)

    @handle_fs_errors
    def setxattr(self,path,name,value,options,position=0):
        try:
            return self.fs.setxattr(path,name,value)
        except AttributeError:
            raise UnsupportedError("setxattr")

    @handle_fs_errors
    def symlink(self, target, source):
        raise UnsupportedError("symlink")

    @handle_fs_errors
    def truncate(self, path, length, fh=None):
        if fh is None and length == 0:
            self.fs.open(path,"w").close()
        else:
            if fh is None:
                f = self.fs.open(path,"w+")
            else:
                f = self._get_file(fh)
            if not hasattr(f,"truncate"):
                raise UnsupportedError("trunace")
            f.truncate(length)

    @handle_fs_errors
    def unlink(self, path):
        self.fs.remove(path)

    @handle_fs_errors
    def utimens(self, path, times=None):
        raise UnsupportedError("utimens")

    @handle_fs_errors
    def write(self, path, data, offset, fh):
        f = self._get_file(fh)
        f.seek(offset)
        f.write(data)
        return len(data)


def mount(fs,path,foreground=False,ready_callback=None,**kwds):
    """Mount the given FS at the given path, using FUSE.

    By default, this function spawns a new background process to manage the
    FUSE event loop.  The return value in this case is an instance of the
    'MountProcess' class, a subprocess.Popen subclass.

    If the keyword argument 'foreground' is given, we instead run the FUSE
    main loop in the current process.  In this case the function will block
    until the filesystem is unmounted, then return None.

    If the keyword argument 'ready_callback' is provided, it will be called
    when the filesystem has been mounted and is ready for use.  Any additional
    keyword arguments will be passed through as options to the underlying
    FUSE class.  Some interesting options include:

        * nothreads:  switch off threading in the FUSE event loop
        * fsname:     name to display in the mount info table

    """
    if foreground:
        ops = FSOperations(fs,on_init=ready_callback)
        return FUSE(ops,path,foreground=foreground,**kwds)
    else:
        mp = MountProcess(fs,path,kwds)
        if ready_callback:
            ready_callback()
        return mp


def unmount(path):
    """Unmount the given mount point.

    This function shells out to the 'fusermount' program to unmount a
    FUSE filesystem.  It works, but it would probably be better to use the
    'unmount' method on the MountProcess class if you have it.
    """
    if os.system("fusermount -u '" + path + "'"):
        raise OSError("filesystem could not be unmounted: " + path)


class MountProcess(subprocess.Popen):
    """subprocess.Popen subclass managing a FUSE mount.

    This is a subclass of subprocess.Popen, designed for easy management of
    a FUSE mount in a background process.  Rather than specifying the command
    to execute, pass in the FS object to be mounted, the target mount point
    and a dictionary of options for the underlying FUSE class.

    In order to be passed successfully to the new process, the FS object
    must be pickleable.  This restriction may be lifted in the future.

    This class has an extra attribute 'path' giving the path to the mounted
    filesystem, and an extra method 'unmount' that will cleanly unmount it
    and terminate the process.

    By default, the spawning process will block until it receives notification
    that the filesystem has been mounted.  Since this notification is sent
    by writing to a pipe, using the 'close_fds' option on this class will
    prevent it from being sent.  You can also pass in the keyword argument
    'nowait' to continue without waiting for notification.

    """

    #  This works by spawning a new python interpreter and passing it the
    #  pickled (fs,path,opts) tuple on the command-line.  Something like this:
    #
    #    python -c "import MountProcess; MountProcess._do_mount('..data..')
    #
    #  It would be more efficient to do a straight os.fork() here, and would
    #  remove the need to pickle the FS.  But API wise, I think it's much
    #  better for mount() to return a Popen instance than just a pid.
    #
    #  In the future this class could implement its own forking logic and
    #  just copy the relevant bits of the Popen interface.  For now, this
    #  spawn-a-new-interpreter solution is the easiest to get up and running.

    def __init__(self,fs,path,fuse_opts={},nowait=False,**kwds):
        self.path = path
        if nowait or kwds.get("close_fds",False):
            cmd = 'from fs.expose.fuse import MountProcess; '
            cmd = cmd + 'MountProcess._do_mount_nowait(%s)'
            cmd = cmd % (pickle.dumps((fs,path,fuse_opts)),)
            cmd = cmd % (repr(pickle.dumps((fs,path,fuse_opts),-1)),)
            cmd = [sys.executable,"-c",cmd]
            super(MountProcess,self).__init__(cmd,**kwds)
        else:
            (r,w) = os.pipe()
            cmd = 'from fs.expose.fuse import MountProcess; '
            cmd = cmd + 'MountProcess._do_mount_wait(%s)'
            cmd = cmd % (repr(pickle.dumps((fs,path,fuse_opts,r,w),-1)),)
            cmd = [sys.executable,"-c",cmd]
            super(MountProcess,self).__init__(cmd,**kwds)
            os.close(w)
            os.read(r,1)

    def unmount(self):
        """Cleanly unmount the FUSE filesystem, terminating this subprocess."""
        if hasattr(self,"terminate"):
            self.terminate()
        else:
            os.kill(self.pid,signal.SIGTERM)

    @staticmethod
    def _do_mount_nowait(data):
        """Perform the specified mount, return without waiting."""
        (fs,path,opts) = pickle.loads(data)
        opts["foreground"] = True
        mount(fs,path,*opts)

    @staticmethod
    def _do_mount_wait(data):
        """Perform the specified mount, signalling when ready."""
        (fs,path,opts,r,w) = pickle.loads(data)
        os.close(r)
        opts["foreground"] = True
        opts["ready_callback"] = lambda: os.close(w)
        mount(fs,path,**opts)


if __name__ == "__main__":
    import os, os.path
    from fs.tempfs import TempFS
    mount_point = os.path.join(os.environ["HOME"],"fs.expose.fuse")
    if not os.path.exists(mount_point):
        os.makedirs(mount_point)
    def ready_callback():
        print "READY"
    mount(TempFS(),mount_point,foreground=True,ready_callback=ready_callback)