Merge branch '16552-autocert'
[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
84 class StringFile(File):
85     """Wrap a simple string as a file"""
86     def __init__(self, parent_inode, contents, _mtime):
87         super(StringFile, self).__init__(parent_inode, _mtime)
88         self.contents = contents
89
90     def size(self):
91         return len(self.contents)
92
93     def readfrom(self, off, size, num_retries=0):
94         return bytes(self.contents[off:(off+size)], encoding='utf-8')
95
96
97 class ObjectFile(StringFile):
98     """Wrap a dict as a serialized json object."""
99
100     def __init__(self, parent_inode, obj):
101         super(ObjectFile, self).__init__(parent_inode, "", 0)
102         self.object_uuid = obj['uuid']
103         self.update(obj)
104
105     def uuid(self):
106         return self.object_uuid
107
108     def update(self, obj=None):
109         if obj is None:
110             # TODO: retrieve the current record for self.object_uuid
111             # from the server. For now, at least don't crash when
112             # someone tells us it's a good time to update but doesn't
113             # pass us a fresh obj. See #8345
114             return
115         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
116         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
117
118     def persisted(self):
119         return True
120
121
122 class FuncToJSONFile(StringFile):
123     """File content is the return value of a given function, encoded as JSON.
124
125     The function is called at the time the file is read. The result is
126     cached until invalidate() is called.
127     """
128     def __init__(self, parent_inode, func):
129         super(FuncToJSONFile, self).__init__(parent_inode, "", 0)
130         self.func = func
131
132         # invalidate_inode() is asynchronous with no callback to wait for. In
133         # order to guarantee userspace programs don't get stale data that was
134         # generated before the last invalidate(), we must disallow inode
135         # caching entirely.
136         self.allow_attr_cache = False
137
138     def size(self):
139         self._update()
140         return super(FuncToJSONFile, self).size()
141
142     def readfrom(self, *args, **kwargs):
143         self._update()
144         return super(FuncToJSONFile, self).readfrom(*args, **kwargs)
145
146     def _update(self):
147         if not self.stale():
148             return
149         self._mtime = time.time()
150         obj = self.func()
151         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
152         self.fresh()