Improve FUSE driver with write and delete (#292)

* Add write to FUSE driver

* Update code

* Fix file creation

* Remove debug print

* Remove more print statements

* Remove unsupported function

* Fix entries

* Allocate space for new dir entry

* Fix dir block size computation

* Add unlink

* Refactor private methods

* Remove debug operation
This commit is contained in:
Vincent Ollivier 2022-04-15 13:00:54 +02:00 committed by GitHub
parent 512b60564b
commit 407d20c75c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 176 additions and 35 deletions

View File

@ -2,6 +2,7 @@
import logging import logging
import os import os
from time import time
from errno import ENOENT from errno import ENOENT
from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from fuse import FUSE, FuseOSError, Operations, LoggingMixIn
from stat import S_IFDIR, S_IFREG from stat import S_IFDIR, S_IFREG
@ -9,24 +10,22 @@ from stat import S_IFDIR, S_IFREG
BLOCK_SIZE = 512 BLOCK_SIZE = 512
SUPERBLOCK_ADDR = 4096 * BLOCK_SIZE SUPERBLOCK_ADDR = 4096 * BLOCK_SIZE
BITMAP_ADDR = SUPERBLOCK_ADDR + 2 * BLOCK_SIZE BITMAP_ADDR = SUPERBLOCK_ADDR + 2 * BLOCK_SIZE
ENTRY_SIZE = (1 + 4 + 4 + 8 + 1)
class MorosFuse(LoggingMixIn, Operations): class MorosFuse(LoggingMixIn, Operations):
chmod = None chmod = None
chown = None chown = None
create = None
mkdir = None
readlink = None readlink = None
rename = None rename = None
rmdir = None rmdir = None
symlink = None symlink = None
truncate = None truncate = None
unlink = None
utimens = None utimens = None
write = None getxattr = None
def __init__(self, path): def __init__(self, path):
self.block_size = BLOCK_SIZE self.block_size = BLOCK_SIZE
self.image = open(path, "rb") self.image = open(path, "r+b")
self.image.seek(SUPERBLOCK_ADDR) self.image.seek(SUPERBLOCK_ADDR)
superblock = self.image.read(self.block_size) superblock = self.image.read(self.block_size)
assert superblock[0:8] == b"MOROS FS" # Signature assert superblock[0:8] == b"MOROS FS" # Signature
@ -40,39 +39,54 @@ class MorosFuse(LoggingMixIn, Operations):
rest = (total - (BITMAP_ADDR // self.block_size)) * bs // (bs + 1) rest = (total - (BITMAP_ADDR // self.block_size)) * bs // (bs + 1)
self.data_addr = BITMAP_ADDR + (rest // bs) * self.block_size self.data_addr = BITMAP_ADDR + (rest // bs) * self.block_size
def destroy(self, path): def __next_free_addr(self):
self.image.close() for bitmap_addr in range(BITMAP_ADDR, self.data_addr):
return self.image.seek(bitmap_addr)
bitmap = self.image.read(self.block_size)
for i in range(self.block_size):
byte = bitmap[i]
for bit in range(0, 8):
if (byte >> bit & 1) == 0:
block = (bitmap_addr - BITMAP_ADDR) * self.block_size * 8 + i * 8 + bit
return self.data_addr + block * self.block_size
def getattr(self, path, fh=None): def __is_alloc(self, addr):
(kind, addr, size, time, name) = self.__scan(path) block = (addr - self.data_addr) // self.block_size
if addr == 0: bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8))
raise FuseOSError(ENOENT) pos = bitmap_addr + (block // 8)
mode = S_IFDIR | 0o755 if kind == 0 else S_IFREG | 0o644 self.image.seek(pos)
return { "st_atime": 0, "st_mtime": time, "st_uid": 0, "st_gid": 0, "st_mode": mode, "st_size": size } byte = int.from_bytes(self.image.read(1), "big")
bit = block % 8
return (byte >> bit & 1) == 1
def read(self, path, size, offset, fh): def __alloc(self, addr):
(kind, next_block_addr, size, time, name) = self.__scan(path) block = (addr - self.data_addr) // self.block_size
res = b"" bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8))
while next_block_addr != 0: self.image.seek(bitmap_addr + (block // 8))
self.image.seek(next_block_addr) byte = int.from_bytes(self.image.read(1), "big")
next_block_addr = int.from_bytes(self.image.read(4), "big") * self.block_size self.image.seek(-1, 1)
if offset < self.block_size - 4: bit = block % 8
buf = self.image.read(min(self.block_size - 4, size)) byte |= (1 << bit)
res = b"".join([res, buf[offset:]]) self.image.write(bytes([byte]))
offset = 0
else:
offset -= self.block_size - 4
size -= self.block_size - 4
return res
def readdir(self, path, fh): def __free(self, addr):
files = [".", ".."] block = (addr - self.data_addr) // self.block_size
(_, next_block_addr, _, _, _) = self.__scan(path) bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8))
for (kind, addr, size, time, name) in self.__read(next_block_addr): self.image.seek(bitmap_addr + (block // 8))
files.append(name) byte = int.from_bytes(self.image.read(1), "big")
return files self.image.seek(-1, 1)
bit = block % 8
byte &= ~(1 << bit)
self.image.write(bytes([byte]))
def __update_dir_size(self, path, entries):
entries.remove(".")
entries.remove("..")
(_, parent_addr, parent_size, _, parent_name) = self.__scan(path)
parent_size = ENTRY_SIZE * len(entries) + len("".join(entries))
self.image.seek(-(4 + 8 + 1 + len(parent_name)), 1)
self.image.write(parent_size.to_bytes(4, "big"))
return parent_addr
def __scan(self, path): def __scan(self, path):
next_block_addr = self.data_addr next_block_addr = self.data_addr
@ -107,6 +121,133 @@ class MorosFuse(LoggingMixIn, Operations):
if addr > 0: if addr > 0:
yield (kind, addr, size, time, name) yield (kind, addr, size, time, name)
def destroy(self, path):
self.image.close()
return
def getattr(self, path, fh=None):
(kind, addr, size, time, name) = self.__scan(path)
if addr == 0:
raise FuseOSError(ENOENT)
mode = S_IFDIR | 0o755 if kind == 0 else S_IFREG | 0o644
return { "st_atime": 0, "st_mtime": time, "st_uid": 0, "st_gid": 0, "st_mode": mode, "st_size": size }
def read(self, path, size, offset, fh):
(kind, next_block_addr, size, time, name) = self.__scan(path)
res = b""
while next_block_addr != 0 and size > 0:
self.image.seek(next_block_addr)
next_block_addr = int.from_bytes(self.image.read(4), "big") * self.block_size
if offset < self.block_size - 4:
buf = self.image.read(max(0, min(self.block_size - 4, size)))
res = b"".join([res, buf[offset:]])
offset = 0
else:
offset -= self.block_size - 4
size -= self.block_size - 4
return res
def readdir(self, path, fh):
files = [".", ".."]
(_, next_block_addr, _, _, _) = self.__scan(path)
for (kind, addr, size, time, name) in self.__read(next_block_addr):
files.append(name)
return files
def mkdir(self, path, mode):
self.create(path, S_IFDIR | mode)
def create(self, path, mode):
(path, _, name) = path.rpartition("/")
entries = self.readdir(path + "/", 0)
entries.append(name)
pos = self.image.tell()
parent_addr = self.__update_dir_size(path, entries)
# Allocate space for the new dir entry if needed
blocks = 0
addr = parent_addr
next_addr = parent_addr
while next_addr != 0:
addr = next_addr
blocks += 1
self.image.seek(addr)
next_addr = int.from_bytes(self.image.read(4), "big") * self.block_size
free_size = (self.block_size - 4) - (pos - addr)
if free_size < ENTRY_SIZE + len(name):
pos = self.image.tell() - 4
addr = self.__next_free_addr()
self.__alloc(addr)
self.image.seek(pos)
self.image.write((addr // self.block_size).to_bytes(4, "big"))
pos = addr + 4
# Allocate space for the new file
kind = int((mode & S_IFDIR) != S_IFDIR)
size = 0
addr = self.__next_free_addr()
self.__alloc(addr)
# Add dir entry
self.image.seek(pos)
self.image.write(kind.to_bytes(1, "big"))
self.image.write((addr // self.block_size).to_bytes(4, "big"))
self.image.write(size.to_bytes(4, "big"))
self.image.write(int(time()).to_bytes(8, "big"))
self.image.write(len(name).to_bytes(1, "big"))
self.image.write(name.encode("utf-8"))
return 0
def write(self, path, data, offset, fh):
(_, addr, size, _, name) = self.__scan(path)
n = self.block_size - 4 # Space available for data in blocks
j = size % n # Start of space available in last block
# Update file size
self.image.seek(-(4 + 8 + 1 + len(name)), 1)
size = max(size, offset + len(data))
self.image.write(size.to_bytes(4, "big"))
for i in range(0, offset, n):
self.image.seek(addr)
next_addr = int.from_bytes(self.image.read(4), "big") * self.block_size
if i + n >= offset:
self.image.seek(addr + 4 + j)
self.image.write(data[0:(n - j)])
if next_addr == 0:
next_addr = self.__next_free_addr()
self.__alloc(next_addr)
self.image.seek(addr)
self.image.write((next_addr // self.block_size).to_bytes(4, "big"))
addr = next_addr
for i in range(n - j if j > 0 else 0, len(data), n):
next_addr = 0
if i + n < len(data): # TODO: check for off by one error
next_addr = self.__next_free_addr()
self.__alloc(next_addr)
self.image.seek(addr)
self.image.write((next_addr // self.block_size).to_bytes(4, "big"))
self.image.write(data[i:min(i + n, len(data))])
addr = next_addr
return len(data)
def unlink(self, path):
# Unlink and free blocs
(_, addr, size, _, name) = self.__scan(path)
self.image.seek(-(4 + 4 + 8 + 1 + len(name)), 1)
self.image.write((0).to_bytes(4, "big"))
while addr != 0:
self.__free(addr)
self.image.seek(addr)
addr = int.from_bytes(self.image.read(4), "big") * self.block_size
# Update parent dir size
(path, _, _) = path.rpartition("/")
entries = self.readdir(path + "/", 0)
self.__update_dir_size(path, entries)
if __name__ == '__main__': if __name__ == '__main__':
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -114,4 +255,4 @@ if __name__ == '__main__':
parser.add_argument('mount') parser.add_argument('mount')
args = parser.parse_args() args = parser.parse_args()
#logging.basicConfig(level=logging.DEBUG) #logging.basicConfig(level=logging.DEBUG)
fuse = FUSE(MorosFuse(args.image), args.mount, ro=True, foreground=True, allow_other=True) fuse = FUSE(MorosFuse(args.image), args.mount, ro=False, foreground=True, allow_other=True)