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