model.py 15.1 KB
Newer Older
1
2
3
4
import llfuse
import time
import os
import stat
5
import copy
6
import logging
7
8
9

import settings

10
11

class FuseIO():
Gerion Entrup's avatar
Gerion Entrup committed
12
13
14
15
16
17
    """Represents a folder or a file.

    The main meaning of this object is to hold a combined class for the
    attribute and inode handling.
    """

18
    def __init__(self, mode, inode, attr=None):
Gerion Entrup's avatar
Gerion Entrup committed
19
20
21
22
23
24
25
        """Construct a FuseIO object.

        Arguments:
        mode  -- the mode of the file
        inode -- the inode of the file
        attr  -- a complete set of attributes, overwrite the mode parameter
        """
26
27
28
29
30
31
32
        self._inode = inode
        if attr:
            self._attr = attr
        else:
            self._attr = self._construct_attr(mode)

    def _construct_attr(self, mode):
Gerion Entrup's avatar
Gerion Entrup committed
33
        """Returns an EntryAttributes object with defaults and given mode."""
34
35
36
37
        attr = llfuse.EntryAttributes()
        attr.st_mode = mode
        attr.st_nlink = 1
        attr.st_size = 0
38

39
40
        # attr.generation = 0
        # attr.st_rdev = 0
41

42
43
44
45
46
        # attr.attr_timeout = 300
        # attr.entry_timeout = 300
        attr.st_atime_ns = int(time.time())
        attr.st_mtime_ns = int(time.time())
        attr.st_ctime_ns = int(time.time())
47

48
49
        # attr.st_blksize = 512
        # attr.st_blocks = 1
50

51
52
        attr.st_gid = os.getgid()
        attr.st_uid = os.getuid()
53

54
        return attr
55
56

    def get_inode(self):
Gerion Entrup's avatar
Gerion Entrup committed
57
        """Return the inode."""
58
59
        if self._inode == -1:
            raise Exception("Special File")
60
        else:
61
62
63
            return self._inode

    def set_inode(self, inode):
Gerion Entrup's avatar
Gerion Entrup committed
64
        """Set the inode."""
65
        self._inode = inode
66

67
    def get_attr(self):
Gerion Entrup's avatar
Gerion Entrup committed
68
        """Return the attributes."""
69
70
71
72
73
        if self._inode == -1:
            if hasattr(self, "_name"):
                raise Exception(self._name + ": Special File")
            raise Exception("Special File")
        self._attr.st_ino = self._inode
74
75
        return self._attr

76
    def add_nlink(self):
Gerion Entrup's avatar
Gerion Entrup committed
77
        """Add an nlink to the attributes."""
78
79
80
81
        self._attr.st_nlink += 1

    def deepcopy_and_register(self, placeholder, obj):
        """ Make a generic deepcopy and register an inode.
Gerion Entrup's avatar
Gerion Entrup committed
82

83
84
85
86
87
88
89
90
91
92
93
94
95
96
        The placeholder and obj parameter make no sense here and are
        for signature compability with FuseDir.
        """
        if self._inode != -1:
            raise Exception("Cannot create a deep copy of an real file.")
        pcopy = copy.copy(self)
        # TODO
        # https://bitbucket.org/nikratio/python-llfuse/issues/95/copying-of-llfuseentryattributes-fails
        # pcopy._attr = copy.copy(self._attr)
        pcopy._attr = self._construct_attr(self._attr.st_mode)
        self._data.pin_inode(pcopy)
        return pcopy

    def is_special(self):
Gerion Entrup's avatar
Gerion Entrup committed
97
        """Indicates whether this object is special. Returns always false."""
98
99
100
101
        return False


class Node:
Gerion Entrup's avatar
Gerion Entrup committed
102
103
    """Represents a node in the filesystem tree."""

104
    def __init__(self, parent, name, data, root=False):
Gerion Entrup's avatar
Gerion Entrup committed
105
106
107
108
109
110
111
112
113
114
        """Constructs a node.

        Arguments:
        parent -- The parent node.
        name   -- The filename or virtual name of the node.
        data   -- The Data object, that mananges the inodes.

        Keyword arguments:
        root -- Indicates whether the node is the root node. (default: False)
        """
115
116
117
118
119
120
121
122
        self._parent = parent
        if root:
            self._parent = self
        self._name = name
        self._data = data
        self._children = {}
        self._specials = []

123
    def get_parent(self):
Gerion Entrup's avatar
Gerion Entrup committed
124
        """Returns the parent of the node."""
Gerion Entrup's avatar
Gerion Entrup committed
125
        return self._parent
126

127
    def get_root(self):
Gerion Entrup's avatar
Gerion Entrup committed
128
        """Returns the root node of the tree/filesystem."""
129
130
131
132
        parent = self
        while parent is not parent.get_parent():
            parent = parent.get_parent()
        return parent
133

134
    def get_name(self):
Gerion Entrup's avatar
Gerion Entrup committed
135
        """Returns the name of the node."""
136
        return self._name
137

138
    def add_special(self, special):
Gerion Entrup's avatar
Gerion Entrup committed
139
        """Adds a Special object to the node."""
140
141
        self._specials.append(special)
        return special
Gerion Entrup's avatar
Gerion Entrup committed
142

143
    def add_fuseio(self, fuseio, filename):
Gerion Entrup's avatar
Gerion Entrup committed
144
        """Adds a FuseIO object to the node."""
145
146
147
148
149
150
151
        if filename in self._children:
            raise Exception("File with filename {} already exists".format(
                filename))
        self._children[filename] = fuseio
        return fuseio

    def is_leaf(self):
Gerion Entrup's avatar
Gerion Entrup committed
152
        """Returns whether the node is a leaf or not."""
153
154
155
        return len(self._children) == 0 and len(self._specials) == 0

    def get_children(self):
Gerion Entrup's avatar
Gerion Entrup committed
156
157
158
159
        """Returns the FuseIO children objects as dict.

        The dict has the filename as key and the object as value.
        """
160
161
162
        return self._children

    def get_specials(self):
Gerion Entrup's avatar
Gerion Entrup committed
163
        """Returns the Special children objects as list."""
164
165
166
167
        return self._specials


class Special(Node):
Gerion Entrup's avatar
Gerion Entrup committed
168
169
    """Represent a node that holds a query and can evolve to real nodes."""

170
171
    def __init__(self, query, functions, where, session,
                 parent, name, data, fileattr):
Gerion Entrup's avatar
Gerion Entrup committed
172
173
174
175
176
177
178
179
180
181
182
183
184
185
        """Construct a special node.

        Arguments:
        query     -- The query that is needed to construct the real nodes.
        functions -- A Functions object to generate and parse filenames.
        where     -- A list of Where objects, to specify the query.
        session   -- A SQLAlchemy session.
        parent    -- Superclass argument, see Node.
        name      -- Superclass argument, see Node.
        data      -- Superclass argument, see Node.
        fileattr  -- A function that extract gid and path for FuseFile
                     generation.
        """

186
187
188
189
190
191
        super().__init__(parent, name, data)
        self._query = query
        self._functions = functions
        self._session = session
        self._where = where
        self._fileattr = fileattr
192
        self.logger = logging.getLogger('sqlfuse.special')
193
194
195
196
197
198
199
200
201
202
203
204
205

    def __repr__(self):
        return ''.join(["Special(query=", repr(self._query),
                        ", functions=",   repr(self._functions),
                        ", where=",       repr(self._where),
                        ", session=",     repr(self._session),
                        ", parent=",      self._parent.get_name(),
                        ", name=",        self._name,
                        ", data=",        repr(self._data),
                        ", fileattr=",    repr(self._fileattr),
                        ")"])

    def query_name(self, qname):
Gerion Entrup's avatar
Gerion Entrup committed
206
207
208
209
210
        """Execute the query with the given qname.

        The function returns the first result if results exists or None
        otherwise.
        """
211
        query = self._functions.parse_name(qname, self._query)
212
        self.logger.debug("query by name '{}': {}".format(qname, str(query)))
213
214
215
216
217
        if query is None:
            return None
        return self._session.execute(query).first()

    def query_all(self):
Gerion Entrup's avatar
Gerion Entrup committed
218
        """Execute the query and return the result."""
219
        self.logger.debug("query all: {}".format(str(self._query)))
220
221
222
        return self._session.execute(self._query)

    def is_special(self):
Gerion Entrup's avatar
Gerion Entrup committed
223
        """Indicates whether this object is special. Returns always false."""
224
225
226
        return True

    def get_fileattr(self, row):
Gerion Entrup's avatar
Gerion Entrup committed
227
        """Return the gid and path attributes of a row."""
228
229
230
        return self._fileattr(row)

    def get_obj_fname(self, row):
Gerion Entrup's avatar
Gerion Entrup committed
231
        """Return the generated filename out of a row."""
232
233
234
        return self._functions.gen_name(row)

    def apply_where(self, placeholder, obj):
Gerion Entrup's avatar
Gerion Entrup committed
235
        """Apply a where clause, where placeholder is replaced with object."""
236
237
        for pwhere in self._where:
            if pwhere.ph_obj == placeholder:
238
239
240
241
242
243
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("apply where clause with function '{}', \
attribute {}, object items: {}".format('.'.join([str(pwhere.function.__self__),
                                                pwhere.function.__name__]),
                                       pwhere.attribute,
                                       obj.items()))
244
245
246
247
248
249
250
251
252
253
                try:
                    attr = getattr(obj, pwhere.attribute)
                except AttributeError:
                    raise Exception("Applying where fails: " + str(obj) +
                                    " has not the wanted attribute " +
                                    pwhere.attribute)
                self._query = self._query.where(pwhere.function(attr))


class FuseDir(FuseIO, Node):
Gerion Entrup's avatar
Gerion Entrup committed
254
255
    """Represents a directory in the filesystem tree."""

256
    def __init__(self, mode, inode, parent, name, data, root=False):
Gerion Entrup's avatar
Gerion Entrup committed
257
258
259
260
261
262
263
264
265
266
267
268
        """Constructs a directory node.

        Arguments:
        mode   -- The read/write/execute rights of the directory.
        inode  -- Superclass argument, see FuseIO.
        parent -- Superclass argument, see Node.
        name   -- Superclass argument, see Node.
        data   -- Superclass argument, see Node.

        Keyword arguments:
        root -- Superclass argument, see Node
        """
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
        if mode is None:
            mode = settings.default_mode_dir
        FuseIO.__init__(self, stat.S_IFDIR | mode, inode)
        Node.__init__(self, parent, name, data, root=root)
        self._attr.st_size = 4096
        self._sorted_filenames = []
        if root:
            self._attr.st_nlink = 2

    def __repr__(self):
        return ''.join(["FuseDir(mode=", oct(self._attr.st_mode),
                        ", inode=",      str(self._inode),
                        ", parent=",     self._parent.get_name(),
                        ", name=",       self._name,
                        ")"])

    def construct_from_query(self, special, row):
Gerion Entrup's avatar
Gerion Entrup committed
286
287
288
289
290
291
292
        """Construct a FuseIO child based on a query result from a
        special object.

        Arguments:
        special -- The special object.
        row     -- The row of the query result.
        """
293
294
295
296
297
298
299
300
301
302
303
304
305
        name = special.get_obj_fname(row)
        if name in self._children:
            return None, None

        if special.is_leaf():
            (gid, path) = special.get_fileattr(row)
            f = FuseFile(None, -1, self, gid, path, self._data)
        else:
            f = FuseDir(None, -1, self, name, self._data)
            for (fname, child) in special.get_children().items():
                f.add_fuseio(child.deepcopy_and_register(special.get_name(),
                                                         row),
                             fname)
306
307
308
309
            for s_special in special.get_specials():
                s_special = copy.copy(s_special)
                s_special.apply_where(special.get_name(), row)
                f.add_special(s_special)
310
311
312
313
314
        self._data.pin_inode(f)
        self.add_fuseio(f, name)
        return name, f

    def deepcopy_and_register(self, placeholder, obj):
Gerion Entrup's avatar
Gerion Entrup committed
315
316
317
318
319
320
321
322
323
        """Constructs a deepcopy of the directory and assign an inode.

        If the directory or a subdirectory contains special nodes with
        where clauses, this where clause are applied via apply_where().

        Arguments:
        placeholder -- The placeholder argument for apply_where().
        obj         -- The obj argument for apply_where().
        """
324
325
326
327
328
329
330
331
332
333
334
335
        pcopy = FuseIO.deepcopy_and_register(self, placeholder, obj)
        pcopy._children = {}
        pcopy._sorted_filenames = []
        pcopy._specials = []
        for (fname, child) in self._children.items():
            pcopy.add_fuseio(child.deepcopy_and_register(placeholder, obj),
                             fname)
        for special in self._specials:
            special = copy.copy(special)
            special.apply_where(placeholder, obj)
            pcopy.add_special(special)
        return pcopy
336

337
    def query_name(self, filename):
Gerion Entrup's avatar
Gerion Entrup committed
338
339
340
341
342
343
344
345
        """Query the directory contents after a given filename.

        This models more less the behaviour of the lookup syscall. If the
        directory contains special objects, the relevant queries are executed.

        Arguments:
        filename -- the filename that will be queried.
        """
Gerion Entrup's avatar
Gerion Entrup committed
346
347
348
        if filename == '.':
            return self.get_attr()
        if filename == '..':
349
            return self.get_parent().get_attr()
Gerion Entrup's avatar
Gerion Entrup committed
350
        if filename in self._children:
351
352
353
354
355
356
            return self._children[filename].get_attr()
        for special in self._specials:
            res = special.query_name(filename)
            if res is not None:
                name, f = self.construct_from_query(special, res)
                return f.get_attr()
Gerion Entrup's avatar
Gerion Entrup committed
357
358

    def add_fuseio(self, fuseio, filename):
Gerion Entrup's avatar
Gerion Entrup committed
359
        """Adds a FuseIO object with a given filename to the directory."""
360
361
362
363
364
365
        Node.add_fuseio(self, fuseio, filename)
        self._sorted_filenames.append(filename)
        fuseio.add_nlink()
        if isinstance(fuseio, FuseDir):
            self.add_nlink()
        return fuseio
366
367

    def opendir(self):
Gerion Entrup's avatar
Gerion Entrup committed
368
        """Will be called by Operations before readdir(), not function yet."""
369
370
371
        pass

    def readdir(self, offset):
Gerion Entrup's avatar
Gerion Entrup committed
372
373
374
        """Read the contents of the directory up from offset. Yields the
        result.
        """
375
376
377
378
379
380
381
382
383
384
385
386
        for child in self._sorted_filenames[offset:]:
            yield (child, self._children[child].get_attr())
        for special in self._specials:
            nlen = len(self._sorted_filenames)
            res = special.query_all()
            for row in res:
                name, f = self.construct_from_query(special, row)
                if name is not None and offset <= nlen:
                        yield(name, f.get_attr())


class FuseFile(FuseIO, Node):
Gerion Entrup's avatar
Gerion Entrup committed
387
388
389
390
391
392
    """Represents a file node and a leaf in the filesystem tree.

    The file is always linked to a real file in a real directory
    and opens and read this file, if it is requested.
    """

393
    def __init__(self, mode, inode, parent, gid, path, data):
Gerion Entrup's avatar
Gerion Entrup committed
394
395
396
397
398
399
400
401
402
403
        """Constructs a file node.

        Arguments:
        mode   -- The read/write/execute rights of the directory.
        inode  -- Superclass argument, see FuseIO.
        parent -- Superclass argument, see Node.
        gid    -- The unique gid of the file.
        path   -- The filepath of the real file.
        data   -- Superclass argument, see Node.
        """
404
405
406
407
408
        if mode is None:
            mode = settings.default_mode_file
        FuseIO.__init__(self, stat.S_IFREG | mode, inode)
        Node.__init__(self, parent, None, data)
        self._gid = gid
409
        self._path = path
410
411
412
413
414
415
416
417
418
        self._attr.st_size = os.path.getsize(path)

    def __repr__(self):
        return ''.join(["FuseFile(mode=", oct(self._attr.st_mode),
                        ", inode=",       str(self._inode),
                        ", parent=",      self._parent.get_name(),
                        ", gid=",         str(self._gid),
                        ", path=",        self._path,
                        ")"])
419
420

    def open(self):
Gerion Entrup's avatar
Gerion Entrup committed
421
        """Opens the file."""
422
423
424
        self._file = open(self._path, 'rb')

    def close(self):
Gerion Entrup's avatar
Gerion Entrup committed
425
        """Closes the file."""
426
        self._file.close()
427

428
    def get_name(self):
Gerion Entrup's avatar
Gerion Entrup committed
429
        """Returns the name (gid) of the file."""
430
431
432
        return str(self._gid)

    def read(self, offset, size):
Gerion Entrup's avatar
Gerion Entrup committed
433
        """Read size bytes up from offset of the file."""
434
435
436
        self._file.seek(offset)
        return self._file.read(size)

437
    def add_fuseio(self, fuseio, filename):
Gerion Entrup's avatar
Gerion Entrup committed
438
        """This method should never be called."""
439
440
441
        raise Exception("FuseFile has to be a leaf.")

    def add_special(self, special):
Gerion Entrup's avatar
Gerion Entrup committed
442
        """This method should never be called."""
443
        raise Exception("FuseFile has to be a leaf.")