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