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