diff --git a/cgfs.py b/cgfs.py index be504b2b..96f776b0 100755 --- a/cgfs.py +++ b/cgfs.py @@ -69,12 +69,10 @@ def wrap_string(string, prefix, max_len): return ('\n').join(res), len(res) - 1 -class DirTypes(IntEnum): - FSROOT = 0 - COURSE = 1 - ASSIGNMENT = 2 - SUBMISSION = 3 - REGDIR = 4 +def split_path(path): + if isinstance(path, list): + return path + return [x for x in path.split('/') if x] class BaseFile(): @@ -83,7 +81,7 @@ def __init__(self, data, name=None): self.name = name if name is not None else data['name'] self.stat = None - def getattr(self, submission=None, path=None): + def getattr(self): if self.stat is None: self.stat = { 'st_size': 0, @@ -94,11 +92,6 @@ def getattr(self, submission=None, path=None): 'st_gid': os.getegid(), } - if submission is not None and path is not None: - stat = cgapi.get_file_meta(submission.id, path) - self.stat['st_size'] = stat['size'] - self.stat['st_mtime'] = stat['modification_date'] - return self.stat def setattr(self, key, value): @@ -109,17 +102,16 @@ def setattr(self, key, value): class Directory(BaseFile): - def __init__(self, data, name=None, type=DirTypes.REGDIR, writable=False): + def __init__(self, data, name=None, writable=False): super(Directory, self).__init__(data, name) - self.type = type self.writable = writable self.children = {} self.children_loaded = False - def getattr(self, submission=None, path=None): + def getattr(self): if self.stat is None: - super(Directory, self).getattr(submission, path) + super(Directory, self).getattr() mode = 0o770 if self.writable else 0o550 self.stat['st_mode'] = S_IFDIR | mode self.stat['st_nlink'] = 2 @@ -143,12 +135,172 @@ def pop(self, filename): return file + def get_file(self, path): + parts = split_path(path) + name = parts[0] + parts = parts[1:] + + if not name in self.children: + raise FuseOSError(ENOENT) + + child = self.children[name] + + if parts: + if not isinstance(child, Directory): + raise FuseOSError(ENOTDIR) + return child.get_file(parts) + else: + return child + def read(self): res = list(self.children) res.extend(['.', '..']) return res +class ServerDirectory(Directory): + def __init__(self, cgapi, *args, **kwargs): + super(ServerDirectory, self).__init__(*args, **kwargs) + self.cgapi = cgapi + + +class RootDirectory(ServerDirectory): + def __init__(self, cgapi): + super(RootDirectory, self).__init__(cgapi, { + 'id': None, + 'name': None, + }) + self.getattr() + + for course in self.cgapi.get_courses(): + course_dir = CourseDirectory(self.cgapi, course) + self.insert(course_dir) + self.children_loaded = True + + +class CourseDirectory(ServerDirectory): + def __init__(self, cgapi, course): + super(CourseDirectory, self).__init__(cgapi, course) + self.getattr() + + for assig in course['assignments']: + assig_dir = AssignmentDirectory(self.cgapi, assig) + assig_dir.insert(AssignmentSettingsFile(self.cgapi, assig['id'])) + assig_dir.insert( + RubricEditorFile( + self.cgapi, assig['id'], rubric_append_only + ) + ) + assig_dir.insert(HelpFile(RubricEditorFile)) + assig_dir.insert( + SpecialFile( + '.cg-assignment-id', + data=str(assig['id']).encode() + b'\n' + ) + ) + self.insert(assig_dir) + self.children_loaded = True + + +class AssignmentDirectory(ServerDirectory): + def __init__(self, cgapi, assig): + super(AssignmentDirectory, self).__init__(cgapi, assig) + self.getattr() + + def load_submissions(self, latest_only=True): + try: + submissions = self.cgapi.get_submissions(self.id) + except CGAPIException as e: # pragma: no cover + handle_cgapi_exception(e) + + seen = set() + + for sub in submissions: + if sub['user']['id'] in seen: + continue + + sub_dir = SubmissionDirectory(self.cgapi, sub) + + if latest_only: + seen.add(sub['user']['id']) + + sub_dir.insert(RubricSelectFile(self.cgapi, sub['id'], sub['user'])) + sub_dir.insert(GradeFile(self.cgapi, sub['id'])) + sub_dir.insert(FeedbackFile(self.cgapi, sub['id'])) + self.insert(sub_dir) + + self.children_loaded = True + + def get_file(self, path): + if not self.children_loaded: + self.load_submissions() + return super(AssignmentDirectory, self).get_file(path) + + def read(self): + if not self.children_loaded: + self.load_submissions() + return super(AssignmentDirectory, self).read() + + +class SubmissionDirectory(ServerDirectory): + def __init__(self, cgapi, sub): + super(SubmissionDirectory, self).__init__( + cgapi, + sub, + name=sub['user']['name'] + ' - ' + sub['created_at'], + writable=True + ) + self.getattr() + + def load_files(self, submission): + try: + files = self.cgapi.get_submission_files(self.id) + except CGAPIException as e: + handle_cgapi_exception(e) + + def insert_tree(dir, tree): + for item in tree['entries']: + if 'entries' in item: + new_dir = RegularDirectory(item) + new_dir.getattr() + dir.insert(new_dir) + insert_tree(new_dir, item) + else: + dir.insert(ServerFile(self.cgapi, item)) + dir.children_loaded = True + + insert_tree(self, files) + self.insert( + SpecialFile( + '.cg-submission-id', data=str(submission.id).encode() + b'\n' + ) + ) + self.tld = files['name'] + self.children_loaded = True + + def get_file(self, path): + if not self.children_loaded: + self.load_files() + return super(SubmissionDirectory, self).get_file(path) + + def read(self): + if not self.children_loaded: + self.load_submission_files(dir) + return super(SubmissionDirectory, self).read() + + def get_full_path(self, path, is_dir=False): + parts = split_path(path) + full_path = self.tld + '/' + '/'.join(parts) + if (is_dir): + full_path += '/' + return full_path + + +class RegularDirectory(Directory): + def __init__(self, data): + super(RegularDirectory, self).__init__(data, writable=True) + + class TempDirectory(Directory): def __init__(self, *args, **kwargs): super(TempDirectory, self).__init__(*args, **kwargs) @@ -258,13 +410,14 @@ def flush(self): class CachedSpecialFile(SpecialFile): DELTA = datetime.timedelta(seconds=60) - def __init__(self, name): + def __init__(self, cgapi, name): super(CachedSpecialFile, self).__init__(name=name) self.data = None self.time = None self.mtime = time() self.mode = 0o770 self.overwrite = False + self.cgapi = cgapi def get_st_mtime(self): return self.mtime @@ -352,13 +505,12 @@ def truncate(self, length): class FeedbackFile(CachedSpecialFile): NAME = '.cg-feedback' - def __init__(self, api, submission_id): - self.api = api - super(FeedbackFile, self).__init__(name=self.NAME) + def __init__(self, cgapi, submission_id): + super(FeedbackFile, self).__init__(cgapi, name=self.NAME) self.submission_id = submission_id def get_online_data(self): - feedback = self.api.get_submission(self.submission_id)['comment'] + feedback = self.cgapi.get_submission(self.submission_id)['comment'] if not feedback: return b'' @@ -382,20 +534,19 @@ def parse(self, data): return ''.join(res).strip() def send_back(self, feedback): - self.api.set_submission(self.submission_id, feedback=feedback) + self.cgapi.set_submission(self.submission_id, feedback=feedback) class GradeFile(CachedSpecialFile): NAME = '.cg-grade' - def __init__(self, api, submission_id): - self.api = api + def __init__(self, cgapi, submission_id): self.grade = None - super(GradeFile, self).__init__(name=self.NAME) + super(GradeFile, self).__init__(cgapi, name=self.NAME) self.submission_id = submission_id def get_online_data(self): - grade = self.api.get_submission(self.submission_id)['grade'] + grade = self.cgapi.get_submission(self.submission_id)['grade'] if grade is None: return b'' @@ -422,23 +573,22 @@ def send_back(self, grade): if grade < 0 or grade > 10: raise FuseOSError(EPERM) - self.api.set_submission(self.submission_id, grade=grade) + self.cgapi.set_submission(self.submission_id, grade=grade) class RubricSelectFile(CachedSpecialFile): NAME = '.cg-rubric.md' - def __init__(self, api, submission_id, user): - super(RubricSelectFile, self).__init__(name=self.NAME) + def __init__(self, cgapi, submission_id, user): + super(RubricSelectFile, self).__init__(cgapi, name=self.NAME) self.submission_id = submission_id self.user = user self.lookup = {} - self.api = api def get_online_data(self): res = [] self.lookup = {} - d = self.api.get_submission_rubric(self.submission_id) + d = self.cgapi.get_submission_rubric(self.submission_id) sel = set(i['id'] for i in d['selected']) l_num = 0 if d['rubrics']: @@ -496,7 +646,7 @@ def parse(self, data): return sel def send_back(self, sel): - self.api.select_rubricitems(self.submission_id, sel) + self.cgapi.select_rubricitems(self.submission_id, sel) class RubricEditorFile(CachedSpecialFile): @@ -555,9 +705,8 @@ class RubricEditorFile(CachedSpecialFile): """ NAME = '.cg-edit-rubric.md' - def __init__(self, api, assignment_id, append_only=True): - super(RubricEditorFile, self).__init__(name=self.NAME) - self.api = api + def __init__(self, cgapi, assignment_id, append_only=True): + super(RubricEditorFile, self).__init__(cgapi, name=self.NAME) self.assignment_id = assignment_id self.append_only = append_only self.lookup = {} @@ -571,7 +720,7 @@ def get_online_data(self): res = [] self.lookup = {} - for rub in self.api.get_assignment_rubric(self.assignment_id): + for rub in self.cgapi.get_assignment_rubric(self.assignment_id): res.append('# ') res.append('[{}] '.format(self.hash_id(rub['id']))) res.append(rub['header']) @@ -758,24 +907,23 @@ def get_from_lookup(h): raise FuseOSError(EPERM) self.lookup = new_lookup - self.api.set_assignment_rubric(self.assignment_id, {'rows': res}) + self.cgapi.set_assignment_rubric(self.assignment_id, {'rows': res}) class AssignmentSettingsFile(CachedSpecialFile): TO_USE = {'state', 'deadline', 'name'} - def __init__(self, api, assignment_id): + def __init__(self, cgapi, assignment_id): super(AssignmentSettingsFile, - self).__init__(name='.cg-assignment-settings.ini') + self).__init__(cgapi, name='.cg-assignment-settings.ini') self.assignment_id = assignment_id - self.api = api def send_back(self, data): - self.api.set_assignment(self.assignment_id, data) + self.cgapi.set_assignment(self.assignment_id, data) def get_online_data(self): lines = [] - for k, v in self.api.get_assignment(self.assignment_id).items(): + for k, v in self.cgapi.get_assignment(self.assignment_id).items(): if k not in self.TO_USE: continue @@ -890,17 +1038,18 @@ def truncate(self, length): self._handle.flush() -class File(BaseFile, SingleFile): - def __init__(self, data, name=None): - super(File, self).__init__(data, name) +class ServerFile(BaseFile, SingleFile): + def __init__(self, cgapi, data, name=None): + super(ServerFile, self).__init__(data, name) + self.cgapi = cgapi self._data = None self.dirty = False @property def data(self): if self._data is None: - self._data = cgapi.get_file(self.id) + self._data = self.cgapi.get_file(self.id) self.stat['st_size'] = len(self._data) return self._data @@ -912,10 +1061,15 @@ def data(self, data): def getattr(self, submission=None, path=None): if self.stat is None: - super(File, self).getattr(submission, path) + super(ServerFile, self).getattr(submission, path) self.stat['st_mode'] = S_IFREG | 0o770 self.stat['st_nlink'] = 1 + if submission is not None and path is not None: + stat = __cgapi__.get_file_meta(submission.id, path) + self.stat['st_size'] = stat['size'] + self.stat['st_mtime'] = stat['modification_date'] + if self.stat['st_size'] is None: self.stat['st_size'] = len(self.data) return self.stat @@ -938,7 +1092,7 @@ def flush(self): assert self._data is not None try: - res = cgapi.patch_file(self.id, self._data) + res = self.cgapi.patch_file(self.id, self._data) except CGAPIException as e: self.data = None self.dirty = False @@ -992,7 +1146,8 @@ def fsync(self): class APIHandler: OPS = {'set_feedback', 'get_feedback', 'delete_feedback', 'is_file'} - def __init__(self, cgfs): + def __init__(self, cgapi, cgfs): + self.cgapi = cgapi self.cgfs = cgfs self.stop = False @@ -1047,7 +1202,7 @@ def delete_feedback(self, payload): return {'ok': False, 'error': 'File not found'} try: - res = cgapi.delete_feedback(f.id, line) + res = self.cgapi.delete_feedback(f.id, line) except: return {'ok': False, 'error': 'The server returned an error'} @@ -1062,7 +1217,7 @@ def is_file(self, payload): except: return {'ok': False, 'error': 'File not found'} - return {'ok': isinstance(f, File)} + return {'ok': isinstance(f, ServerFile)} def get_feedback(self, payload): f_name = self.cgfs.strippath(payload['file']) @@ -1077,7 +1232,7 @@ def get_feedback(self, payload): return {'ok': False, 'error': 'File not a sever file'} try: - res = cgapi.get_feedback(f.id) + res = self.cgapi.get_feedback(f.id) except: return {'ok': False, 'error': 'The server returned an error'} @@ -1098,7 +1253,7 @@ def set_feedback(self, payload): return {'ok': False, 'error': 'File not a sever file'} try: - cgapi.add_feedback(f.id, line, message) + self.cgapi.add_feedback(f.id, line, message) except: return {'ok': False, 'error': 'The server returned an error'} @@ -1110,6 +1265,7 @@ class CGFS(LoggingMixIn, Operations): def __init__( self, + cgapi, latest_only, socketfile, mountpoint, @@ -1118,6 +1274,7 @@ def __init__( rubric_append_only=True, quiet=False, ): + self.cgapi = cgapi self.latest_only = latest_only self.fixed = fixed self.files = {} @@ -1133,7 +1290,7 @@ def __init__( self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket.bind(self._socketfile) self.socket.listen() - self.api_handler = APIHandler(self) + self.api_handler = APIHandler(self.cgapi, self) threading.Thread( target=self.api_handler.run, args=(self.socket, ) ).start() @@ -1143,22 +1300,16 @@ def __init__( self.rubric_append_only = rubric_append_only - self.files = Directory( - { - 'id': None, - 'name': 'root' - }, type=DirTypes.FSROOT - ) + self.files = RootDirectory(cgapi) with self._lock: - self.files.getattr() self.files.insert(self.special_socketfile) self.files.insert( SpecialFile( '.cg-mode', b'FIXED\n' if self.fixed else b'NOT_FIXED\n' ) ) - self.load_courses() + if not self.quiet: print('Mounted') @@ -1166,131 +1317,33 @@ def strippath(self, path): path = os.path.abspath(path) return path[len(self.mountpoint):] - def load_courses(self): - for course in cgapi.get_courses(): - assignments = course['assignments'] - - course_dir = Directory(course, type=DirTypes.COURSE) - course_dir.getattr() - self.files.insert(course_dir) - - for assig in assignments: - assig_dir = Directory(assig, type=DirTypes.ASSIGNMENT) - assig_dir.getattr() - course_dir.insert(assig_dir) - assig_dir.insert(AssignmentSettingsFile(cgapi, assig['id'])) - assig_dir.insert( - RubricEditorFile( - cgapi, assig['id'], self.rubric_append_only - ) - ) - assig_dir.insert(HelpFile(RubricEditorFile)) - assig_dir.insert( - SpecialFile( - '.cg-assignment-id', - data=str(assig['id']).encode() + b'\n' - ) - ) - course_dir.children_loaded = True - self.files.children_loaded = True - - def load_submissions(self, assignment): - try: - submissions = cgapi.get_submissions(assignment.id) - except CGAPIException as e: # pragma: no cover - handle_cgapi_exception(e) - - seen = set() - - for sub in submissions: - if sub['user']['id'] in seen: - continue - - sub_dir = Directory( - sub, - name=sub['user']['name'] + ' - ' + sub['created_at'], - type=DirTypes.SUBMISSION, - writable=True - ) - - if self.latest_only: - seen.add(sub['user']['id']) - - sub_dir.getattr() - sub_dir.insert(RubricSelectFile(cgapi, sub['id'], sub['user'])) - sub_dir.insert(GradeFile(cgapi, sub['id'])) - sub_dir.insert(FeedbackFile(cgapi, sub['id'])) - assignment.insert(sub_dir) - - assignment.children_loaded = True - - def insert_tree(self, dir, tree): - for item in tree['entries']: - if 'entries' in item: - new_dir = Directory(item, writable=True) - new_dir.getattr() - dir.insert(new_dir) - self.insert_tree(new_dir, item) - else: - dir.insert(File(item)) - dir.children_loaded = True - - def load_submission_files(self, submission): - try: - files = cgapi.get_submission_files(submission.id) - except CGAPIException as e: - handle_cgapi_exception(e) - self.insert_tree(submission, files) - submission.insert( - SpecialFile( - '.cg-submission-id', data=str(submission.id).encode() + b'\n' - ) - ) - submission.tld = files['name'] - submission.children_loaded = True - - def split_path(self, path): - return [x for x in path.split('/') if x] - def get_submission(self, path): - parts = self.split_path(path) + parts = split_path(path) submission = self.get_file(parts[:3]) - try: - submission.tld - except AttributeError: - self.load_submission_files(submission) + + if not isinstance(submission, SubmissionDirectory): + raise FuseOSError(ENOENT) + if not submission.children_loaded: + submission.load_submission_files() return submission def get_file(self, path, start=None, expect_type=None): file = start if start is not None else self.files - parts = self.split_path(path) if isinstance(path, str) else path - for part in parts: - if part == '': # pragma: no cover - continue - - try: - if not any( - not isinstance(f, SpecialFile) - for f in file.children.values() - ): - if file.type == DirTypes.ASSIGNMENT: - self.load_submissions(file) - elif file.type == DirTypes.SUBMISSION: - self.load_submission_files(file) - except AttributeError: # pragma: no cover - if not isinstance(file, Directory): - raise FuseOSError(ENOTDIR) - raise + if not isinstance(file, Directory): + raise FuseOSError(ENOTDIR) - if part not in file.children or file.children[part] is None: - raise FuseOSError(ENOENT) - file = file.children[part] + parts = split_path(path) + if parts: + file = file.get_file(parts) if expect_type is not None: if not isinstance(file, expect_type): - raise FuseOSError(EISDIR) + if issubclass(expect_type, Directory): + raise FuseOSError(ENOTDIR) + elif issubclass(expect_type, SingleFile): + raise FuseOSError(EISDIR) return file @@ -1308,7 +1361,7 @@ def create(self, path, mode): return self._create(path, mode) def _create(self, path, mode): - parts = self.split_path(path) + parts = split_path(path) if len(parts) <= 3: raise FuseOSError(EPERM) @@ -1318,17 +1371,17 @@ def _create(self, path, mode): assert fname not in parent.children submission = self.get_submission(path) - query_path = submission.tld + '/' + '/'.join(parts[3:]) + query_path = submission.get_full_path(parts[3:]) if self.fixed: file = TempFile(fname, self._tmpdir) else: try: - fdata = cgapi.create_file(submission.id, query_path) + fdata = self.cgapi.create_file(submission.id, query_path) except CGAPIException as e: handle_cgapi_exception(e) - file = File(fdata, name=fname) + file = ServerFile(fdata, name=fname) file.setattr('st_size', fdata['size']) file.setattr('st_mtime', fdata['modification_date']) @@ -1363,32 +1416,30 @@ def getattr(self, path, fh=None): def _getattr(self, path, fh): if fh is None: - parts = self.split_path(path) + parts = split_path(path) file = self.get_file(parts) else: file = self._open_files[fh] + print('file', file) + if isinstance(file, (TempFile, SpecialFile)): return file.getattr() - if file.stat is None and len(parts) > 3: + if file.stat is None and isinstance(file, ServerFile): try: submission = self.get_submission(path) except CGAPIException as e: handle_cgapi_exception(e) - query_path = submission.tld + '/' + '/'.join(parts[3:]) + query_path = submission.get_full_path(parts[3:]) - if isinstance(file, Directory): - query_path += '/' - else: - submission = None - query_path = None + attrs = file.getattr(submission, query_path) + if self.fixed: + attrs['st_mode'] &= ~0o222 + return attrs - attrs = file.getattr(submission, query_path) - if self.fixed and isinstance(file, File): - attrs['st_mode'] &= ~0o222 - return attrs + return file.getattr() # TODO?: Add xattr support def getxattr(self, path, name, position=0): @@ -1403,7 +1454,7 @@ def mkdir(self, path, mode): return self._mkdir(path, mode) def _mkdir(self, path, mode): - parts = self.split_path(path) + parts = split_path(path) parent = self.get_dir(parts[:-1]) dname = parts[-1] @@ -1415,8 +1466,8 @@ def _mkdir(self, path, mode): parent.insert(TempDirectory({}, name=dname, writable=True)) else: submission = self.get_submission(path) - query_path = submission.tld + '/' + '/'.join(parts[3:]) + '/' - ddata = cgapi.create_file(submission.id, query_path) + query_path = submission.get_full_path(parts[3:], is_dir=True) + ddata = self.cgapi.create_file(submission.id, query_path) parent.insert(Directory(ddata, name=dname, writable=True)) @@ -1425,7 +1476,7 @@ def open(self, path, flags): return self._open(path, flags) def _open(self, path, flags): - parts = self.split_path(path) + parts = split_path(path) parent = self.get_dir(parts[:-1]) file = self.get_file(parts[-1], start=parent, expect_type=SingleFile) @@ -1451,13 +1502,6 @@ def read(self, path, size, offset, fh): def readdir(self, path, fh): with self._lock: dir = self.get_dir(path) - - if not dir.children_loaded: - if dir.type == DirTypes.ASSIGNMENT: - self.load_submissions(dir) - elif dir.type == DirTypes.SUBMISSION: - self.load_submission_files(dir) - return dir.read() def readlink(self, path): @@ -1478,14 +1522,14 @@ def rename(self, old, new): self._rename(old, new) def _rename(self, old, new): - old_parts = self.split_path(old) + old_parts = split_path(old) old_parent = self.get_dir(old_parts[:-1]) file = self.get_file(old_parts[-1], start=old_parent) if isinstance(file, SpecialFile): raise FuseOSError(EPERM) - new_parts = self.split_path(new) + new_parts = split_path(new) new_parent = self.get_dir(new_parts[:-1]) if new_parts[-1] in new_parent.children: @@ -1498,14 +1542,14 @@ def _rename(self, old, new): if submission.id != self.get_submission(new).id: raise FuseOSError(EPERM) - new_query_path = submission.tld + '/' + '/'.join(new_parts[3:]) + '/' + new_query_path = submission.full_path(new_parts[3:]) + '/' if not isinstance(file, (TempDirectory, TempFile)): if self.fixed: raise FuseOSError(EPERM) try: - res = cgapi.rename_file(file.id, new_query_path) + res = self.cgapi.rename_file(file.id, new_query_path) except CGAPIException as e: handle_cgapi_exception(e) @@ -1520,11 +1564,11 @@ def rmdir(self, path): self._rmdir(path) def _rmdir(self, path): - parts = self.split_path(path) + parts = split_path(path) parent = self.get_dir(parts[:-1]) dir = self.get_file(parts[-1], start=parent) - if dir.type != DirTypes.REGDIR: + if not isinstance(dir, RegularDirectory): raise FuseOSError(EPERM) if dir.children: raise FuseOSError(ENOTEMPTY) @@ -1534,7 +1578,7 @@ def _rmdir(self, path): raise FuseOSError(EPERM) try: - cgapi.delete_file(dir.id) + self.cgapi.delete_file(dir.id) except CGAPIException as e: handle_cgapi_exception(e) @@ -1573,7 +1617,7 @@ def truncate(self, path, length, fh=None): def unlink(self, path): with self._lock: - parts = self.split_path(path) + parts = split_path(path) parent = self.get_dir(parts[:-1]) fname = parts[-1] file = self.get_file(fname, start=parent, expect_type=SingleFile) @@ -1588,7 +1632,7 @@ def unlink(self, path): raise FuseOSError(EPERM) try: - cgapi.delete_file(file.id) + self.cgapi.delete_file(file.id) except CGAPIException as e: handle_cgapi_exception(e) @@ -1600,7 +1644,7 @@ def utimens(self, path, times=None): assert file is not None atime, mtime = times or (time(), time()) - if isinstance(file, File) and self.fixed: + if isinstance(file, ServerFile) and self.fixed: raise FuseOSError(EPERM) file.utimens(atime, mtime) @@ -1715,6 +1759,7 @@ def write(self, path, data, offset, fh): sockfile = tempfile.NamedTemporaryFile().name try: fs = CGFS( + cgapi, latest_only, socketfile=sockfile, fixed=fixed,