21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / fuse / arvados_fuse / fusefile.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 from __future__ import absolute_import
6 from builtins import bytes
7 import json
8 import llfuse
9 import logging
10 import re
11 import time
12
13 from .fresh import FreshBase, convertTime
14
15 _logger = logging.getLogger('arvados.arvados_fuse')
16
17 class File(FreshBase):
18     """Base for file objects."""
19
20     __slots__ = ("inode", "parent_inode", "_mtime")
21
22     def __init__(self, parent_inode, _mtime=0):
23         super(File, self).__init__()
24         self.inode = None
25         self.parent_inode = parent_inode
26         self._mtime = _mtime
27
28     def size(self):
29         return 0
30
31     def readfrom(self, off, size, num_retries=0):
32         return ''
33
34     def writeto(self, off, size, num_retries=0):
35         raise Exception("Not writable")
36
37     def mtime(self):
38         return self._mtime
39
40     def clear(self):
41         pass
42
43     def writable(self):
44         return False
45
46     def flush(self):
47         pass
48
49
50 class FuseArvadosFile(File):
51     """Wraps a ArvadosFile."""
52
53     __slots__ = ('arvfile', '_enable_write')
54
55     def __init__(self, parent_inode, arvfile, _mtime, enable_write):
56         super(FuseArvadosFile, self).__init__(parent_inode, _mtime)
57         self.arvfile = arvfile
58         self._enable_write = enable_write
59
60     def size(self):
61         with llfuse.lock_released:
62             return self.arvfile.size()
63
64     def readfrom(self, off, size, num_retries=0):
65         with llfuse.lock_released:
66             return self.arvfile.readfrom(off, size, num_retries, exact=True)
67
68     def writeto(self, off, buf, num_retries=0):
69         with llfuse.lock_released:
70             return self.arvfile.writeto(off, buf, num_retries)
71
72     def stale(self):
73         return False
74
75     def writable(self):
76         return self._enable_write and self.arvfile.writable()
77
78     def flush(self):
79         with llfuse.lock_released:
80             if self.writable():
81                 self.arvfile.parent.root_collection().save()
82
83     def clear(self):
84         if self.parent_inode is None:
85             self.arvfile.fuse_entry = None
86             self.arvfile = None
87
88
89 class StringFile(File):
90     """Wrap a simple string as a file"""
91
92     __slots__ = ("contents",)
93
94     def __init__(self, parent_inode, contents, _mtime):
95         super(StringFile, self).__init__(parent_inode, _mtime)
96         self.contents = contents
97
98     def size(self):
99         return len(self.contents)
100
101     def readfrom(self, off, size, num_retries=0):
102         return bytes(self.contents[off:(off+size)], encoding='utf-8')
103
104
105 class ObjectFile(StringFile):
106     """Wrap a dict as a serialized json object."""
107
108     __slots__ = ("object_uuid",)
109
110     def __init__(self, parent_inode, obj):
111         super(ObjectFile, self).__init__(parent_inode, "", 0)
112         self.object_uuid = obj['uuid']
113         self.update(obj)
114
115     def uuid(self):
116         return self.object_uuid
117
118     def update(self, obj=None):
119         if obj is None:
120             # TODO: retrieve the current record for self.object_uuid
121             # from the server. For now, at least don't crash when
122             # someone tells us it's a good time to update but doesn't
123             # pass us a fresh obj. See #8345
124             return
125         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
126         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
127
128     def persisted(self):
129         return True
130
131
132 class FuncToJSONFile(StringFile):
133     """File content is the return value of a given function, encoded as JSON.
134
135     The function is called at the time the file is read. The result is
136     cached until invalidate() is called.
137     """
138
139     __slots__ = ("func",)
140
141     def __init__(self, parent_inode, func):
142         super(FuncToJSONFile, self).__init__(parent_inode, "", 0)
143         self.func = func
144
145         # invalidate_inode() is asynchronous with no callback to wait for. In
146         # order to guarantee userspace programs don't get stale data that was
147         # generated before the last invalidate(), we must disallow inode
148         # caching entirely.
149         self.allow_attr_cache = False
150
151     def size(self):
152         self._update()
153         return super(FuncToJSONFile, self).size()
154
155     def readfrom(self, *args, **kwargs):
156         self._update()
157         return super(FuncToJSONFile, self).readfrom(*args, **kwargs)
158
159     def _update(self):
160         if not self.stale():
161             return
162         self._mtime = time.time()
163         obj = self.func()
164         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
165         self.fresh()