summaryrefslogtreecommitdiff
path: root/AEpy/AEFuse.py
diff options
context:
space:
mode:
Diffstat (limited to 'AEpy/AEFuse.py')
-rw-r--r--AEpy/AEFuse.py459
1 files changed, 459 insertions, 0 deletions
diff --git a/AEpy/AEFuse.py b/AEpy/AEFuse.py
new file mode 100644
index 0000000..fea6037
--- /dev/null
+++ b/AEpy/AEFuse.py
@@ -0,0 +1,459 @@
+import binascii
+from errno import ENOENT, EINVAL, EIO, EROFS
+from fusepy import FUSE, FuseOSError, Operations
+from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IROTH, S_IXGRP, S_IXOTH, S_IFDIR, S_IFREG
+import struct
+import sys
+from time import time
+
+from AELink import AELinkSerial
+from AESession import AESession
+import AEMultipart
+import AECmds
+from FuseCache import FuseCache
+import FusePatches
+
+
+amiga_charset = 'iso8859-1'
+
+
+
+
+
+
+
+
+class AEFuse(Operations):
+ def __init__(self, session):
+ self.session = session
+
+ self.cache = FuseCache()
+
+ self.readpath = None
+ self.readbuf = None
+ self.readmax = None # Total size of the file being read
+
+ self.writepath = None
+ self.writebuf = None
+ self.writemtime = None
+ self.writeattrs = None
+
+
+ def getattr(self, path, fh=None):
+ print('AEFuse.getattr: ' + path)
+
+ dirpath = path[0:path.rfind('/')] # Without the trailing slash
+ name = path[path.rfind('/') + 1:] # Without a leading slash
+
+ st = self.cache.getattr(path)
+
+ if not st:
+ # If we have a cache miss, see if we've cached its parent
+ # directory.
+ #
+ # If not, then try to read the parent directory.
+ #
+ # Note that accessing a deep path will cause Linux to stat its
+ # parents recursively, which in turn will cause self.getattr()
+ # to call self.readdir() on them, filling all cache levels in
+ # between.
+ #
+ # https://sourceforge.net/p/fuse/mailman/message/19717106/
+ #
+ if self.cache.getkids(dirpath) == None:
+ # We haven't read the directory yet
+ if name in self.readdir(dirpath, fh):
+ # If the dirlist succeeded, try again.
+ #
+ # Note that we're never asking the host for a deep path
+ # that doesn't exist, since Linux will stat all parents,
+ # thus listing directories at every level until the first
+ # one that doesn't exist (see above).
+ #
+ # As a bonus, this automatically updates the cache if the
+ # path now exists.
+ return self.getattr(path, fh)
+
+ # Okay, the path doesn't exist.
+ # That means our file can't exist.
+ print('getaddr ENOENT: ' + path)
+ raise FuseOSError(ENOENT)
+
+ return st
+
+
+ def readdir(self, path, fh):
+ print("AEFuse.readdir: " + path)
+
+ self._cancel_read()
+ self._flush_write()
+
+ # Request directory listing
+ kids = AECmds.dir_list(self.session, path[1:].encode(amiga_charset))
+
+ # Update cache
+ self.cache.setkids(path, kids)
+
+ #return [(name, self.cache[name], 0) for name in self.cache.keys()]
+ return kids.keys()
+
+
+ def read(self, path, size, offset, fh):
+ print("AEFuse.read: " + path + ' -- ' + str(size) + ' @ ' + str(offset))
+
+ # Avoid inconsistency.
+ # If we don't do this, then reading the file while it's open
+ # returns something different from what is in the write buffer.
+ self._flush_write()
+
+ if self.readpath != path:
+ if self.readpath != None:
+ self._cancel_read()
+
+ self.readpath = path
+ self.readbuf = b''
+
+ # Request file
+ self.readmax = AECmds.file_read_start(self.session, path[1:].encode(amiga_charset))
+
+ # TODO: Check for None returned, meaning that the path doesn't exist.
+
+ if (offset + size > len(self.readbuf)) \
+ and (offset < self.readmax) \
+ and (len(self.readbuf) < self.readmax):
+ # Get further file contents if we need it.
+ while len(self.readbuf) < min(offset + size, self.readmax):
+ (blockOffset, blockData) = AECmds.file_read_next(self.session)
+
+ if blockData == None:
+ # Since we only enter this loop if there should be data
+ # left to read, a None returned means an unexpected EOF
+ # or more probably, the connection timed out.
+ #
+ # We operate under the illusion that we can keep the last
+ # accessed file open wherever we were at, however after a
+ # few seconds the host will close the file without us
+ # knowing. In case that happens, let's reload the file.
+ print('Unexpected error while reading next file block.')
+ print('Re-requesting file.')
+
+ # If we cancel the transfer ourselves, then the reply to
+ # sendClose() will be 0x00 0x00 0x00 0x1c 0x00.
+ #
+ # Strange: If we cancel in the same situation, but without
+ # having received the type 1 cancel message from the host,
+ # the reply to sendClose() is normal zeroes.
+ self._cancel_read()
+ return self.read(path, size, offset, fh)
+
+ assert blockOffset == len(self.readbuf)
+ self.readbuf += blockData
+
+ # When done transferring a whole file, the Amiga side insists on
+ # sending its end-of-file type 4 message. So let's request it.
+ if len(self.readbuf) == self.readmax:
+ (blockOffset, blockData) = AECmds.file_read_next(self.session)
+ assert blockData == None
+
+ print("read: " + path + ' -- finished with len(self.readbuf) == ' + str(len(self.readbuf)))
+ return self.readbuf[offset:offset + size]
+
+
+ def _cancel_read(self):
+ # Cancel current read operation
+ if self.readpath != None:
+ if (len(self.readbuf) < self.readmax):
+ print('_cancel_read: Cancelling.')
+ AECmds.cancel(self.session)
+
+ AECmds.close(self.session)
+
+ self.readbuf = None
+ self.readpath = None
+
+
+ def open(self, path, flags):
+ print("AEFuse.open: " + path)
+
+ # Dummy function to allow file access.
+
+ return 0
+
+
+ def _prep_write(self, path, trunclen):
+ # We can't write in the virtual top level directory
+ if len(path) < 4:
+ raise FuseOSError(ENOENT)
+
+ self._cancel_read()
+
+ if self.writepath != path:
+ self._flush_write()
+
+ # If the file already exists, read its previous contents.
+ # Check cache, since FUSE/VFS will have filled it.
+ st = self.cache.getattr(path)
+ if st != None and st['st_size'] > 0:
+ if trunclen != None:
+ if trunclen > 0:
+ self.writebuf = self.read(path, trunclen, 0, None)
+ else:
+ self.writebuf = self.read(path, st['st_size'], 0, None)
+ self._cancel_read()
+
+ self.writepath = path
+ self.writeattrs = 0
+ if self.writebuf == None:
+ self.writebuf = b''
+
+
+ def create(self, path, mode, fi=None):
+ print("AEFuse.create: " + path)
+
+ # We can't write in the virtual top level directory
+ if len(path) < 4:
+ raise FuseOSError(ENOENT)
+
+ # We can't create a file over an existing one.
+ # By now, the system will have primed our cache, so we won't ask
+ # the big fuseop self.getattr(). That wouldn't work anyway, since
+ # it raises a FuseOSError rather than returning None.
+ assert self.cache.getattr(path) == None
+ if self.writepath != None:
+ assert path != self.writepath
+
+ # Create a dummy file, so the path exists
+ AECmds.file_write(self.session, path[1:].encode(amiga_charset), 0, time(), b'')
+
+ # Refresh cache so a subsequent getattr() succeeds
+ dirpath = path[0:path.rfind('/')]
+ self.readdir(dirpath, None)
+
+ return 0
+
+
+ def truncate(self, path, length, fh=None):
+ print("AEFuse.truncate: " + path + ' -- ' + str(length))
+
+ self._prep_write(path, length)
+
+ self.writebuf = self.writebuf[:length]
+ if length > len(self.writebuf):
+ # Fill up
+ self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (length - len(self.writebuf)))
+
+ self.writemtime = time()
+
+ return 0
+
+
+
+ # WARNING: Due to buffering, we will NOT be able to report
+ # a full disk as a response to the write call!
+ def write(self, path, data, offset, fh):
+ print("AEFuse.write: " + path + ' -- ' + str(len(data)) + ' @ ' + str(offset))
+
+ self._prep_write(path, None)
+
+ if offset > len(self.writebuf):
+ # Fill up
+ self.writebuf = self.writebuf[:len(self.writebuf)] + (b'\0' * (offset - len(self.writebuf)))
+
+ self.writebuf = self.writebuf[:offset] + data + self.writebuf[offset+len(data):]
+ self.writemtime = time()
+
+ # WARNING!
+ # We cannot check for a full disk here, so we're
+ # just assuming that buffering is fine.
+ return len(data)
+
+
+ def utimens(self, path, (atime, mtime)):
+ print("AEFuse.utimens: " + path + ' -- ' + str(mtime))
+
+ # We can only set the time when sending a complete file.
+ if self.writepath == path:
+ self.writemtime = mtime
+ else:
+ raise FuseOSError(EROFS)
+
+
+ def _flush_write(self):
+ if self.writepath == None:
+ return
+
+ # If this fails, there is nothing we can do.
+ # Except returning an error on close().
+ # But honestly, who checks for that?
+ AECmds.file_write(self.session,
+ self.writepath[1:].encode(amiga_charset), self.writeattrs,
+ self.writemtime, self.writebuf)
+
+ # Extract dirpath before we throw away self.writepath
+ dirpath = self.writepath[0:self.writepath.rfind('/')]
+
+ # Throw away the write buffer once done, so we don't cache full files.
+ self.writepath = None
+ self.writebuf = None
+ self.writemtime = None
+ self.writeattrs = None
+
+ # Refresh cache so subsequent accesses are correct
+ self.readdir(dirpath, None)
+
+
+ # Called on close()
+ def flush(self, path, fh):
+ print("AEFuse.flush: " + path)
+
+ # Flush any remaining write buffer
+ self._flush_write()
+
+ print("AEFuse.flush: finished: " + path)
+
+
+ def unlink(self, path):
+ print("AEFuse.unlink: " + path)
+
+ # TODO: Handle file that is currently open for reading/writing
+
+ self.session.sendMsg(0x67, path[1:].encode(amiga_charset) + b'\x00')
+
+ (type, _) = self.session.recvMsg()
+
+ self.session.sendClose()
+
+ # Refresh cache so a subsequent getattr() is up to date.
+ # We refresh even in case the delete failed, because it
+ # indicates our cache being out of date.
+ dirpath = path[0:path.rfind('/')]
+ self.readdir(dirpath, None)
+
+ if type == 0:
+ return 0 # File deleted successfully
+ else:
+ raise FuseOSError(EIO)
+
+
+ def rmdir(self, path):
+ # TODO: Refuse if the directory is not empty
+ return self.unlink(path)
+
+
+ def ae_mkdir(self, path):
+ self._cancel_read()
+
+ amigaattrs = 0
+
+ _utime = time() - (2922*24*60*60) # Amiga epoch starts 1978-01-01
+ mdays = int(_utime / (24*60*60)) # Days since 1978-01-01
+ _utime -= mdays * (24*60*60)
+ mmins = int(_utime / 60) # Minutes
+ _utime -= mmins * 60
+ mticks = _utime * 50 # Ticks of 1/50 sec
+
+ filetype = 2 # Folder
+
+ data = struct.pack('!LLLLLLLB',
+ 29 + len(path) + 6, # Length of this message, really
+ 0,
+ 0,
+ amigaattrs,
+ mdays,
+ mmins,
+ mticks,
+ filetype)
+ data += path.encode(amiga_charset)
+ data += b'\0\0\0\0\0\0' # No idea what this is for
+ self.session.sendMsg(0x66, data)
+
+ # TODO: Handle target FS full
+ # TODO: Handle permission denied on target FS
+
+ self.session.sendClose()
+
+
+ def mkdir(self, path, mode):
+ print('AEFuse.mkdir: ' + path)
+
+ self.ae_mkdir(path[1:].encode(amiga_charset))
+
+ # Refresh cache so a subsequent getattr() succeeds
+ dirpath = path[0:path.rfind('/')]
+ self.readdir(dirpath, None)
+
+
+ def rename(self, old, new):
+ print('AEFuse.rename: ' + old + ' - ' + new)
+ dirpath_old = old[0:old.rfind('/')]
+ dirpath_new = new[0:new.rfind('/')]
+ filename_old = old[old.rfind('/') + 1:]
+ filename_new = new[new.rfind('/') + 1:]
+
+ # If the file already exists, delete it.
+ # Check cache, since FUSE/VFS will have filled it.
+ st = self.cache.getattr(new)
+ if st != None:
+ self.unlink(new)
+
+ if dirpath_new == dirpath_old:
+ AECmds.rename(self.session,
+ old[1:].encode(amiga_charset),
+ filename_new.encode(amiga_charset))
+ else:
+ # Move in 3 steps:
+ # 1. Rename file to a temporary name
+ # (NOTE: We hope it doesn't exist!)
+ # 2. Move the file to the new folder
+ # 3. Rename the file to the target file name
+ # The reason for this is that the old file name may exist in
+ # the new directory, and the new file name may exist in the
+ # old directory. Thus, if we were to do only one renaming,
+ # either order of rename+move or move+rename could be
+ # problematic.
+
+ assert (self.cache.getattr(dirpath_old + '/_fatemp_') == None)
+ assert (self.cache.getattr(dirpath_new + '/_fatemp_') == None)
+
+ AECmds.rename(self.session,
+ old[1:].encode(amiga_charset),
+ '_fatemp_'.encode(amiga_charset))
+ AECmds.move(self.session,
+ (dirpath_old + '/_fatemp_')[1:].encode(amiga_charset),
+ dirpath_new[1:].encode(amiga_charset))
+ AECmds.rename(self.session,
+ (dirpath_new + '/_fatemp_')[1:].encode(amiga_charset),
+ filename_new.encode(amiga_charset))
+
+ # Refresh cache so a subsequent getattr() succeeds
+ self.readdir(dirpath_old, None)
+ self.readdir(dirpath_new, None)
+
+
+ def chmod(self, path, mode):
+ print("AEFuse.chmod: " + path + ' -- ' + str(mode))
+
+ amigaattrs = 0
+ # Apparently we don't have to worry about directory flags
+ if not mode & S_IRUSR: amigaattrs |= 0x08
+ if not mode & S_IWUSR: amigaattrs |= 0x04
+ if not mode & S_IXUSR: amigaattrs |= 0x02
+
+ AECmds.setattr(self.session,
+ amigaattrs,
+ path[1:].encode(amiga_charset),
+ '')
+
+
+ def chown(self, path, uid, gid):
+ # AmigaOS does not know users/groups.
+ raise FuseOSError(EROFS)
+
+
+ # Called on umount
+ def destroy(self, path):
+ print('AEFuse.destroy: ' + path)
+
+ # Flush any remaining write buffer
+ self._cancel_read()
+ self._flush_write()