Merge branch '15319-api-useful-stacktraces'
[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',)
52
53     def __init__(self, parent_inode, arvfile, _mtime):
54         super(FuseArvadosFile, self).__init__(parent_inode, _mtime)
55         self.arvfile = arvfile
56
57     def size(self):
58         with llfuse.lock_released:
59             return self.arvfile.size()
60
61     def readfrom(self, off, size, num_retries=0):
62         with llfuse.lock_released:
63             return self.arvfile.readfrom(off, size, num_retries, exact=True)
64
65     def writeto(self, off, buf, num_retries=0):
66         with llfuse.lock_released:
67             return self.arvfile.writeto(off, buf, num_retries)
68
69     def stale(self):
70         return False
71
72     def writable(self):
73         return self.arvfile.writable()
74
75     def flush(self):
76         with llfuse.lock_released:
77             if self.writable():
78                 self.arvfile.parent.root_collection().save()
79
80
81 class StringFile(File):
82     """Wrap a simple string as a file"""
83     def __init__(self, parent_inode, contents, _mtime):
84         super(StringFile, self).__init__(parent_inode, _mtime)
85         self.contents = contents
86
87     def size(self):
88         return len(self.contents)
89
90     def readfrom(self, off, size, num_retries=0):
91         return self.contents[off:(off+size)]
92
93
94 class ObjectFile(StringFile):
95     """Wrap a dict as a serialized json object."""
96
97     def __init__(self, parent_inode, obj):
98         super(ObjectFile, self).__init__(parent_inode, "", 0)
99         self.object_uuid = obj['uuid']
100         self.update(obj)
101
102     def uuid(self):
103         return self.object_uuid
104
105     def update(self, obj=None):
106         if obj is None:
107             # TODO: retrieve the current record for self.object_uuid
108             # from the server. For now, at least don't crash when
109             # someone tells us it's a good time to update but doesn't
110             # pass us a fresh obj. See #8345
111             return
112         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
113         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
114
115     def persisted(self):
116         return True
117
118
119 class FuncToJSONFile(StringFile):
120     """File content is the return value of a given function, encoded as JSON.
121
122     The function is called at the time the file is read. The result is
123     cached until invalidate() is called.
124     """
125     def __init__(self, parent_inode, func):
126         super(FuncToJSONFile, self).__init__(parent_inode, "", 0)
127         self.func = func
128
129         # invalidate_inode() is asynchronous with no callback to wait for. In
130         # order to guarantee userspace programs don't get stale data that was
131         # generated before the last invalidate(), we must disallow inode
132         # caching entirely.
133         self.allow_attr_cache = False
134
135     def size(self):
136         self._update()
137         return super(FuncToJSONFile, self).size()
138
139     def readfrom(self, *args, **kwargs):
140         self._update()
141         return super(FuncToJSONFile, self).readfrom(*args, **kwargs)
142
143     def _update(self):
144         if not self.stale():
145             return
146         self._mtime = time.time()
147         obj = self.func()
148         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
149         self.fresh()