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 _mode_to_arwed(self, mode):
45 if not mode & S_IRUSR: amigaattrs |= 0x08
46 if not mode & S_IWUSR: amigaattrs |= 0x04
47 if not mode & S_IXUSR: amigaattrs |= 0x02
52 def getattr(self, path, fh=None):
53 print('AEFuse.getattr: ' + path)
55 dirpath = path[0:path.rfind('/')] # Without the trailing slash
56 name = path[path.rfind('/') + 1:] # Without a leading slash
58 st = self.cache.getattr(path)
61 # If we have a cache miss, see if we've cached its parent
64 # If not, then try to read the parent directory.
66 # Note that accessing a deep path will cause Linux to stat its
67 # parents recursively, which in turn will cause self.getattr()
68 # to call self.readdir() on them, filling all cache levels in
71 # https://sourceforge.net/p/fuse/mailman/message/19717106/
73 if self.cache.getkids(dirpath) == None:
74 # We haven't read the directory yet
75 if name in self.readdir(dirpath, fh):
76 # If the dirlist succeeded, try again.
78 # Note that we're never asking the host for a deep path
79 # that doesn't exist, since Linux will stat all parents,
80 # thus listing directories at every level until the first
81 # one that doesn't exist (see above).
83 # As a bonus, this automatically updates the cache if the
85 return self.getattr(path, fh)
87 # Okay, the path doesn't exist.
88 # That means our file can't exist.
89 print('getaddr ENOENT: ' + path)
90 raise FuseOSError(ENOENT)
95 def readdir(self, path, fh):
96 print("AEFuse.readdir: " + path)
101 # Request directory listing
102 kids = AECmds.dir_list(self.session, path[1:].encode(amiga_charset))
105 self.cache.setkids(path, kids)
107 #return [(name, self.cache[name], 0) for name in self.cache.keys()]
111 def read(self, path, size, offset, fh):
112 print("AEFuse.read: " + path + ' -- ' + str(size) + ' @ ' + str(offset))
114 # Avoid inconsistency.
115 # If we don't do this, then reading the file while it's open
116 # returns something different from what is in the write buffer.
119 if self.readpath != path:
120 if self.readpath != None:
127 self.readmax = AECmds.file_read_start(self.session, path[1:].encode(amiga_charset))
129 # TODO: Check for None returned, meaning that the path doesn't exist.
131 if (offset + size > len(self.readbuf)) \
132 and (offset < self.readmax) \
133 and (len(self.readbuf) < self.readmax):
134 # Get further file contents if we need it.
135 while len(self.readbuf) < min(offset + size, self.readmax):
136 (blockOffset, blockData) = AECmds.file_read_next(self.session)
138 if blockData == None:
139 # Since we only enter this loop if there should be data
140 # left to read, a None returned means an unexpected EOF
141 # or more probably, the connection timed out.
143 # We operate under the illusion that we can keep the last
144 # accessed file open wherever we were at, however after a
145 # few seconds the host will close the file without us
146 # knowing. In case that happens, let's reload the file.
147 print('Unexpected error while reading next file block.')
148 print('Re-requesting file.')
150 # If we cancel the transfer ourselves, then the reply to
151 # sendClose() will be 0x00 0x00 0x00 0x1c 0x00.
153 # Strange: If we cancel in the same situation, but without
154 # having received the type 1 cancel message from the host,
155 # the reply to sendClose() is normal zeroes.
157 return self.read(path, size, offset, fh)
159 assert blockOffset == len(self.readbuf)
160 self.readbuf += blockData
162 # When done transferring a whole file, the Amiga side insists on
163 # sending its end-of-file type 4 message. So let's request it.
164 if len(self.readbuf) == self.readmax:
165 (blockOffset, blockData) = AECmds.file_read_next(self.session)
166 assert blockData == None
168 print("read: " + path + ' -- finished with len(self.readbuf) == ' + str(len(self.readbuf)))
169 return self.readbuf[offset:offset + size]
172 def _cancel_read(self):
173 # Cancel current read operation
174 if self.readpath != None:
175 if (len(self.readbuf) < self.readmax):
176 print('_cancel_read: Cancelling.')
177 AECmds.cancel(self.session)
179 AECmds.close(self.session)
185 def open(self, path, flags):
186 print("AEFuse.open: " + path)
188 # Dummy function to allow file access.
193 def _prep_write(self, path, trunclen):
194 # We can't write in the virtual top level directory
196 raise FuseOSError(ENOENT)
200 if self.writepath != path:
203 # If the file already exists, read its previous contents.
204 # Check cache, since FUSE/VFS will have filled it.
205 st = self.cache.getattr(path)
206 if st != None and st['st_size'] > 0:
209 self.writebuf = self.read(path, trunclen, 0, None)
211 self.writebuf = self.read(path, st['st_size'], 0, None)
214 self.writepath = path
216 if self.writebuf == None:
220 def create(self, path, mode, fi=None):
221 print("AEFuse.create: " + path)
223 # We can't write in the virtual top level directory
225 raise FuseOSError(ENOENT)
227 # We can't create a file over an existing one.
228 # By now, the system will have primed our cache, so we won't ask
229 # the big fuseop self.getattr(). That wouldn't work anyway, since
230 # it raises a FuseOSError rather than returning None.
231 assert self.cache.getattr(path) == None
232 if self.writepath != None:
233 assert path != self.writepath
235 # Create a dummy file, so the path exists
236 AECmds.file_write(self.session,
237 path[1:].encode(amiga_charset),
238 self._mode_to_arwed(mode),
242 # Refresh cache so a subsequent getattr() succeeds
243 dirpath = path[0:path.rfind('/')]
244 self.readdir(dirpath, None)
249 def truncate(self, path, length, fh=None):
250 print("AEFuse.truncate: " + path + ' -- ' + str(length))
252 self._prep_write(path, length)
254 self.writebuf = self.writebuf[:length]
255 if length > len(self.writebuf):
257 self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (length - len(self.writebuf)))
259 self.writemtime = time()
265 # WARNING: Due to buffering, we will NOT be able to report
266 # a full disk as a response to the write call!
267 def write(self, path, data, offset, fh):
268 print("AEFuse.write: " + path + ' -- ' + str(len(data)) + ' @ ' + str(offset))
270 self._prep_write(path, None)
272 if offset > len(self.writebuf):
274 self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (offset - len(self.writebuf)))
276 self.writebuf = self.writebuf[:offset] + data + self.writebuf[offset+len(data):]
277 self.writemtime = time()
280 # We cannot check for a full disk here, so we're
281 # just assuming that buffering is fine.
285 def utimens(self, path, (atime, mtime)):
286 print("AEFuse.utimens: " + path + ' -- ' + str(mtime))
288 # We can only set the time when sending a complete file.
289 if self.writepath == path:
290 self.writemtime = mtime
292 raise FuseOSError(EROFS)
295 def _flush_write(self):
296 if self.writepath == None:
299 # If this fails, there is nothing we can do.
300 # Except returning an error on close().
301 # But honestly, who checks for that?
302 AECmds.file_write(self.session,
303 self.writepath[1:].encode(amiga_charset),
305 self.writemtime, self.writebuf)
307 # Extract dirpath before we throw away self.writepath
308 dirpath = self.writepath[0:self.writepath.rfind('/')]
310 # Throw away the write buffer once done, so we don't cache full files.
311 self.writepath = None
313 self.writemtime = None
314 self.writeattrs = None
316 # Refresh cache so subsequent accesses are correct
317 self.readdir(dirpath, None)
321 def flush(self, path, fh):
322 print("AEFuse.flush: " + path)
324 # Flush any remaining write buffer
327 print("AEFuse.flush: finished: " + path)
330 def unlink(self, path):
331 print("AEFuse.unlink: " + path)
333 # TODO: Handle file that is currently open for reading/writing
335 self.session.sendMsg(0x67, path[1:].encode(amiga_charset) + b'\x00')
337 (type, _) = self.session.recvMsg()
339 self.session.sendClose()
341 # Refresh cache so a subsequent getattr() is up to date.
342 # We refresh even in case the delete failed, because it
343 # indicates our cache being out of date.
344 dirpath = path[0:path.rfind('/')]
345 self.readdir(dirpath, None)
348 return 0 # File deleted successfully
350 raise FuseOSError(EIO)
353 def rmdir(self, path):
354 # TODO: Refuse if the directory is not empty
355 return self.unlink(path)
358 def ae_mkdir(self, path):
363 _utime = time() - (2922*24*60*60) # Amiga epoch starts 1978-01-01
364 mdays = int(_utime / (24*60*60)) # Days since 1978-01-01
365 _utime -= mdays * (24*60*60)
366 mmins = int(_utime / 60) # Minutes
368 mticks = _utime * 50 # Ticks of 1/50 sec
370 filetype = 2 # Folder
372 data = struct.pack('!LLLLLLLB',
373 29 + len(path) + 6, # Length of this message, really
381 data += path.encode(amiga_charset)
382 data += b'\0\0\0\0\0\0' # No idea what this is for
383 self.session.sendMsg(0x66, data)
385 # TODO: Handle target FS full
386 # TODO: Handle permission denied on target FS
388 self.session.sendClose()
391 def mkdir(self, path, mode):
392 print('AEFuse.mkdir: ' + path)
394 self.ae_mkdir(path[1:].encode(amiga_charset))
396 # Refresh cache so a subsequent getattr() succeeds
397 dirpath = path[0:path.rfind('/')]
398 self.readdir(dirpath, None)
401 def rename(self, old, new):
402 print('AEFuse.rename: ' + old + ' - ' + new)
403 dirpath_old = old[0:old.rfind('/')]
404 dirpath_new = new[0:new.rfind('/')]
405 filename_old = old[old.rfind('/') + 1:]
406 filename_new = new[new.rfind('/') + 1:]
408 # If the file already exists, delete it.
409 # Check cache, since FUSE/VFS will have filled it.
410 st = self.cache.getattr(new)
414 if dirpath_new == dirpath_old:
415 AECmds.rename(self.session,
416 old[1:].encode(amiga_charset),
417 filename_new.encode(amiga_charset))
420 # 1. Rename file to a temporary name
421 # (NOTE: We hope it doesn't exist!)
422 # 2. Move the file to the new folder
423 # 3. Rename the file to the target file name
424 # The reason for this is that the old file name may exist in
425 # the new directory, and the new file name may exist in the
426 # old directory. Thus, if we were to do only one renaming,
427 # either order of rename+move or move+rename could be
430 assert (self.cache.getattr(dirpath_old + '/_fatemp_') == None)
431 assert (self.cache.getattr(dirpath_new + '/_fatemp_') == None)
433 AECmds.rename(self.session,
434 old[1:].encode(amiga_charset),
435 '_fatemp_'.encode(amiga_charset))
436 AECmds.move(self.session,
437 (dirpath_old + '/_fatemp_')[1:].encode(amiga_charset),
438 dirpath_new[1:].encode(amiga_charset))
439 AECmds.rename(self.session,
440 (dirpath_new + '/_fatemp_')[1:].encode(amiga_charset),
441 filename_new.encode(amiga_charset))
443 # Refresh cache so a subsequent getattr() succeeds
444 self.readdir(dirpath_old, None)
445 self.readdir(dirpath_new, None)
448 def chmod(self, path, mode):
449 print("AEFuse.chmod: " + path + ' -- ' + str(mode))
451 # Apparently we don't have to worry about directory flags
452 amigaattrs = self._mode_to_arwed(mode)
454 if path == self.writepath:
455 self.writeattrs = amigaattrs;
457 AECmds.setattr(self.session,
459 path[1:].encode(amiga_charset),
463 def chown(self, path, uid, gid):
464 # AmigaOS does not know users/groups.
465 # Pretend it does, to keep cp from spamming stderr.
470 def destroy(self, path):
471 print('AEFuse.destroy: ' + path)
473 # Flush any remaining write buffer