2 from errno import ENOENT, EINVAL, EIO, EROFS
3 from fusepy import FUSE, FuseOSError, Operations
4 from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IROTH, S_IXGRP, S_IXOTH, S_IFDIR, S_IFREG
9 from AELink import AELinkSerial
10 from AESession import AESession
13 from FuseCache import FuseCache
17 amiga_charset = 'iso8859-1'
26 class AEFuse(Operations):
27 def __init__(self, session):
28 self.session = session
30 self.cache = FuseCache()
34 self.readmax = None # Total size of the file being read
38 self.writemtime = None
39 self.writeattrs = None
42 def getattr(self, path, fh=None):
43 print('AEFuse.getattr: ' + path)
45 dirpath = path[0:path.rfind('/')] # Without the trailing slash
46 name = path[path.rfind('/') + 1:] # Without a leading slash
48 st = self.cache.getattr(path)
51 # If we have a cache miss, see if we've cached its parent
54 # If not, then try to read the parent directory.
56 # Note that accessing a deep path will cause Linux to stat its
57 # parents recursively, which in turn will cause self.getattr()
58 # to call self.readdir() on them, filling all cache levels in
61 # https://sourceforge.net/p/fuse/mailman/message/19717106/
63 if self.cache.getkids(dirpath) == None:
64 # We haven't read the directory yet
65 if name in self.readdir(dirpath, fh):
66 # If the dirlist succeeded, try again.
68 # Note that we're never asking the host for a deep path
69 # that doesn't exist, since Linux will stat all parents,
70 # thus listing directories at every level until the first
71 # one that doesn't exist (see above).
73 # As a bonus, this automatically updates the cache if the
75 return self.getattr(path, fh)
77 # Okay, the path doesn't exist.
78 # That means our file can't exist.
79 print('getaddr ENOENT: ' + path)
80 raise FuseOSError(ENOENT)
85 def readdir(self, path, fh):
86 print("AEFuse.readdir: " + path)
91 # Request directory listing
92 kids = AECmds.dir_list(self.session, path[1:].encode(amiga_charset))
95 self.cache.setkids(path, kids)
97 #return [(name, self.cache[name], 0) for name in self.cache.keys()]
101 def read(self, path, size, offset, fh):
102 print("AEFuse.read: " + path + ' -- ' + str(size) + ' @ ' + str(offset))
104 # Avoid inconsistency.
105 # If we don't do this, then reading the file while it's open
106 # returns something different from what is in the write buffer.
109 if self.readpath != path:
110 if self.readpath != None:
117 self.readmax = AECmds.file_read_start(self.session, path[1:].encode(amiga_charset))
119 # TODO: Check for None returned, meaning that the path doesn't exist.
121 if (offset + size > len(self.readbuf)) \
122 and (offset < self.readmax) \
123 and (len(self.readbuf) < self.readmax):
124 # Get further file contents if we need it.
125 while len(self.readbuf) < min(offset + size, self.readmax):
126 (blockOffset, blockData) = AECmds.file_read_next(self.session)
128 if blockData == None:
129 # Since we only enter this loop if there should be data
130 # left to read, a None returned means an unexpected EOF
131 # or more probably, the connection timed out.
133 # We operate under the illusion that we can keep the last
134 # accessed file open wherever we were at, however after a
135 # few seconds the host will close the file without us
136 # knowing. In case that happens, let's reload the file.
137 print('Unexpected error while reading next file block.')
138 print('Re-requesting file.')
140 # If we cancel the transfer ourselves, then the reply to
141 # sendClose() will be 0x00 0x00 0x00 0x1c 0x00.
143 # Strange: If we cancel in the same situation, but without
144 # having received the type 1 cancel message from the host,
145 # the reply to sendClose() is normal zeroes.
147 return self.read(path, size, offset, fh)
149 assert blockOffset == len(self.readbuf)
150 self.readbuf += blockData
152 # When done transferring a whole file, the Amiga side insists on
153 # sending its end-of-file type 4 message. So let's request it.
154 if len(self.readbuf) == self.readmax:
155 (blockOffset, blockData) = AECmds.file_read_next(self.session)
156 assert blockData == None
158 print("read: " + path + ' -- finished with len(self.readbuf) == ' + str(len(self.readbuf)))
159 return self.readbuf[offset:offset + size]
162 def _cancel_read(self):
163 # Cancel current read operation
164 if self.readpath != None:
165 if (len(self.readbuf) < self.readmax):
166 print('_cancel_read: Cancelling.')
167 AECmds.cancel(self.session)
169 AECmds.close(self.session)
175 def open(self, path, flags):
176 print("AEFuse.open: " + path)
178 # Dummy function to allow file access.
183 def _prep_write(self, path, trunclen):
184 # We can't write in the virtual top level directory
186 raise FuseOSError(ENOENT)
190 if self.writepath != path:
193 # If the file already exists, read its previous contents.
194 # Check cache, since FUSE/VFS will have filled it.
195 st = self.cache.getattr(path)
196 if st != None and st['st_size'] > 0:
199 self.writebuf = self.read(path, trunclen, 0, None)
201 self.writebuf = self.read(path, st['st_size'], 0, None)
204 self.writepath = path
206 if self.writebuf == None:
210 def create(self, path, mode, fi=None):
211 print("AEFuse.create: " + path)
213 # We can't write in the virtual top level directory
215 raise FuseOSError(ENOENT)
217 # We can't create a file over an existing one.
218 # By now, the system will have primed our cache, so we won't ask
219 # the big fuseop self.getattr(). That wouldn't work anyway, since
220 # it raises a FuseOSError rather than returning None.
221 assert self.cache.getattr(path) == None
222 if self.writepath != None:
223 assert path != self.writepath
225 # Create a dummy file, so the path exists
226 AECmds.file_write(self.session, path[1:].encode(amiga_charset), 0, time(), b'')
228 # Refresh cache so a subsequent getattr() succeeds
229 dirpath = path[0:path.rfind('/')]
230 self.readdir(dirpath, None)
235 def truncate(self, path, length, fh=None):
236 print("AEFuse.truncate: " + path + ' -- ' + str(length))
238 self._prep_write(path, length)
240 self.writebuf = self.writebuf[:length]
241 if length > len(self.writebuf):
243 self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (length - len(self.writebuf)))
245 self.writemtime = time()
251 # WARNING: Due to buffering, we will NOT be able to report
252 # a full disk as a response to the write call!
253 def write(self, path, data, offset, fh):
254 print("AEFuse.write: " + path + ' -- ' + str(len(data)) + ' @ ' + str(offset))
256 self._prep_write(path, None)
258 if offset > len(self.writebuf):
260 self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (offset - len(self.writebuf)))
262 self.writebuf = self.writebuf[:offset] + data + self.writebuf[offset+len(data):]
263 self.writemtime = time()
266 # We cannot check for a full disk here, so we're
267 # just assuming that buffering is fine.
271 def utimens(self, path, (atime, mtime)):
272 print("AEFuse.utimens: " + path + ' -- ' + str(mtime))
274 # We can only set the time when sending a complete file.
275 if self.writepath == path:
276 self.writemtime = mtime
278 raise FuseOSError(EROFS)
281 def _flush_write(self):
282 if self.writepath == None:
285 # If this fails, there is nothing we can do.
286 # Except returning an error on close().
287 # But honestly, who checks for that?
288 AECmds.file_write(self.session,
289 self.writepath[1:].encode(amiga_charset), self.writeattrs,
290 self.writemtime, self.writebuf)
292 # Extract dirpath before we throw away self.writepath
293 dirpath = self.writepath[0:self.writepath.rfind('/')]
295 # Throw away the write buffer once done, so we don't cache full files.
296 self.writepath = None
298 self.writemtime = None
299 self.writeattrs = None
301 # Refresh cache so subsequent accesses are correct
302 self.readdir(dirpath, None)
306 def flush(self, path, fh):
307 print("AEFuse.flush: " + path)
309 # Flush any remaining write buffer
312 print("AEFuse.flush: finished: " + path)
315 def unlink(self, path):
316 print("AEFuse.unlink: " + path)
318 # TODO: Handle file that is currently open for reading/writing
320 self.session.sendMsg(0x67, path[1:].encode(amiga_charset) + b'\x00')
322 (type, _) = self.session.recvMsg()
324 self.session.sendClose()
326 # Refresh cache so a subsequent getattr() is up to date.
327 # We refresh even in case the delete failed, because it
328 # indicates our cache being out of date.
329 dirpath = path[0:path.rfind('/')]
330 self.readdir(dirpath, None)
333 return 0 # File deleted successfully
335 raise FuseOSError(EIO)
338 def rmdir(self, path):
339 # TODO: Refuse if the directory is not empty
340 return self.unlink(path)
343 def ae_mkdir(self, path):
348 _utime = time() - (2922*24*60*60) # Amiga epoch starts 1978-01-01
349 mdays = int(_utime / (24*60*60)) # Days since 1978-01-01
350 _utime -= mdays * (24*60*60)
351 mmins = int(_utime / 60) # Minutes
353 mticks = _utime * 50 # Ticks of 1/50 sec
355 filetype = 2 # Folder
357 data = struct.pack('!LLLLLLLB',
358 29 + len(path) + 6, # Length of this message, really
366 data += path.encode(amiga_charset)
367 data += b'\0\0\0\0\0\0' # No idea what this is for
368 self.session.sendMsg(0x66, data)
370 # TODO: Handle target FS full
371 # TODO: Handle permission denied on target FS
373 self.session.sendClose()
376 def mkdir(self, path, mode):
377 print('AEFuse.mkdir: ' + path)
379 self.ae_mkdir(path[1:].encode(amiga_charset))
381 # Refresh cache so a subsequent getattr() succeeds
382 dirpath = path[0:path.rfind('/')]
383 self.readdir(dirpath, None)
386 def rename(self, old, new):
387 print('AEFuse.rename: ' + old + ' - ' + new)
388 dirpath_old = old[0:old.rfind('/')]
389 dirpath_new = new[0:new.rfind('/')]
390 filename_old = old[old.rfind('/') + 1:]
391 filename_new = new[new.rfind('/') + 1:]
393 # If the file already exists, delete it.
394 # Check cache, since FUSE/VFS will have filled it.
395 st = self.cache.getattr(new)
399 if dirpath_new == dirpath_old:
400 AECmds.rename(self.session,
401 old[1:].encode(amiga_charset),
402 filename_new.encode(amiga_charset))
405 # 1. Rename file to a temporary name
406 # (NOTE: We hope it doesn't exist!)
407 # 2. Move the file to the new folder
408 # 3. Rename the file to the target file name
409 # The reason for this is that the old file name may exist in
410 # the new directory, and the new file name may exist in the
411 # old directory. Thus, if we were to do only one renaming,
412 # either order of rename+move or move+rename could be
415 assert (self.cache.getattr(dirpath_old + '/_fatemp_') == None)
416 assert (self.cache.getattr(dirpath_new + '/_fatemp_') == None)
418 AECmds.rename(self.session,
419 old[1:].encode(amiga_charset),
420 '_fatemp_'.encode(amiga_charset))
421 AECmds.move(self.session,
422 (dirpath_old + '/_fatemp_')[1:].encode(amiga_charset),
423 dirpath_new[1:].encode(amiga_charset))
424 AECmds.rename(self.session,
425 (dirpath_new + '/_fatemp_')[1:].encode(amiga_charset),
426 filename_new.encode(amiga_charset))
428 # Refresh cache so a subsequent getattr() succeeds
429 self.readdir(dirpath_old, None)
430 self.readdir(dirpath_new, None)
433 def chmod(self, path, mode):
434 print("AEFuse.chmod: " + path + ' -- ' + str(mode))
437 # Apparently we don't have to worry about directory flags
438 if not mode & S_IRUSR: amigaattrs |= 0x08
439 if not mode & S_IWUSR: amigaattrs |= 0x04
440 if not mode & S_IXUSR: amigaattrs |= 0x02
442 AECmds.setattr(self.session,
444 path[1:].encode(amiga_charset),
448 def chown(self, path, uid, gid):
449 # AmigaOS does not know users/groups.
450 raise FuseOSError(EROFS)
454 def destroy(self, path):
455 print('AEFuse.destroy: ' + path)
457 # Flush any remaining write buffer