This commit is contained in:
Suzanne Soy 2023-09-11 22:10:26 +01:00
parent 7f6d86d8db
commit 4aa92683e0
11 changed files with 150 additions and 29 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
/cache/ /cache/
/mnt/ /mnt/
/test_cache
/test_mnt
/test_source
/test_actual_result

154
fs.py
View File

@ -1,10 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import os, sys, shutil, pathlib import os, sys, errno, shutil, pathlib
from fuse import FUSE, FuseOSError, Operations from fuse import FUSE, FuseOSError, Operations
from collections import namedtuple from collections import namedtuple
Translator = namedtuple('Translator', ['mode', 'output_filename', 'command']) Translator = namedtuple('Translator', ['mode', 'output_filename', 'command'])
# todo: rename 'path' to 'get'
Entity = namedtuple('Entity', ['path', 'is_translator', 'peek', 'source', 'size'])
class FilterFS(Operations): class FilterFS(Operations):
def __init__(self, source, cache): def __init__(self, source, cache):
@ -23,23 +25,6 @@ class FilterFS(Operations):
output_filename = components[1], # TODO: forbid ../ attacks output_filename = components[1], # TODO: forbid ../ attacks
command = '/'.join(components[2:]) command = '/'.join(components[2:])
) )
def _peek(self, why, path):
print('peek', path)
p = os.path.join(self.source, path.lstrip('/'))
if os.path.islink(p):
rl = os.readlink(p)
if rl.startswith('/!/'):
get = os.path.join(self.cache, 'get', path.lstrip('/'))
peek = os.path.join(self.cache, 'peek', path.lstrip('/'))
if os.path.exists(get):
p = get
else:
translator = self._parse(rl)
p = peek
self._mkparents(p)
pathlib.Path(p).touch()
os.chmod(p, translator.mode)
return p
def _get(self, why, path): def _get(self, why, path):
print('get', why, path) print('get', why, path)
@ -62,6 +47,28 @@ class FilterFS(Operations):
p = cached_output p = cached_output
return p return p
def _get_source(self, why, path):
print("_get_source", why, path)
return os.path.join(self.source, path.lstrip('/'))
def _get_entity(self, why, path):
print("_get_entity", why, path)
p = os.path.join(self.source, path.lstrip('/'))
if os.path.islink(p):
rl = os.readlink(p)
if rl.startswith('/!/'):
get = os.path.join(self.cache, 'get', path.lstrip('/'))
peek = os.path.join(self.cache, 'peek', path.lstrip('/'))
if os.path.exists(get):
return Entity(path = get, is_translator = True, peek = peek, source = p, size = None)
else:
translator = self._parse(rl)
self._mkparents(peek)
pathlib.Path(peek).touch()
os.chmod(peek, translator.mode)
return Entity(path = peek, is_translator = True, peek = peek, source = p, size = 9999999999999999)
return Entity(path = p, is_translator = False, peek = p, source = p, size = None)
# directory # directory
def readdir(self, path, fh): def readdir(self, path, fh):
yield '.' yield '.'
@ -70,29 +77,118 @@ class FilterFS(Operations):
if os.path.isdir(orig): if os.path.isdir(orig):
for d in os.listdir(orig): for d in os.listdir(orig):
yield d yield d
def mkdir(self, path, mode):
pass
def rmdir(self, path):
pass
# directory, file etc. # directory, file etc
def getattr(self, path, file_handle = None): def getattr(self, path, file_handle = None):
st = os.lstat(self._peek("getattr", path)) entity = self._get_entity("getattr", path)
attr = os.lstat(entity.path)
print(entity.path)
return { return {
'st_mode': st.st_mode, # 0o100775 file, 0o40775 dir 'st_mode': attr.st_mode, # 0o100775 file, 0o40775 dir
#'st_ino': 42, #'st_ino': 42,
#'st_dev': 123, #'st_dev': 123,
'st_nlink': st.st_nlink, 'st_nlink': attr.st_nlink,
'st_uid': st.st_uid, 'st_uid': attr.st_uid,
'st_gid': st.st_gid, 'st_gid': attr.st_gid,
'st_size': 999999999999999, #st.st_size, # TODO: max file size 'st_size': attr.st_size if entity.size is None else entity.size,
'st_atime': st.st_atime, 'st_atime': attr.st_atime,
'st_mtime': st.st_mtime, 'st_mtime': attr.st_mtime,
'st_ctime': st.st_ctime, 'st_ctime': attr.st_ctime,
} }
def access(self, path, mode):
entity = self._get_entity("access", path)
if not os.access(entity.path, mode):
raise FuseOSError(errno.EACCES)
def chmod(self, path, mode):
return os.chmod(self._get_entity('chmod', path).peek, mode)
def chown(self, path, uid, gid):
return os.chown(self._get_source('chown', path), uid, gid)
def rename(self, old_path, new_path):
old_entity = self._get_entity('rename old', old_path)
new_entity = self._get_entity('rename new', new_path)
if os.path.exists(new_entity.source):
raise FuseOSError(errno.EACCES)
else:
if entity.is_translator:
# TODO: preserve the cache for moved file?
os.unlink(old_entity.path)
os.unlink(old_entity.peek)
return os.rename(old_entity.source, new_entity.source)
else:
return os.unlink(entity.path, mode)
def utimens(self, path, times=None):
# TODO: the "peek" should have an utime in addition to a chmod
return os.utime(self._get_entity('utimens', path).peek, times)
def unlink(self, path):
entity = self._get_entity('unlink', path)
if entity.is_translator:
os.unlink(entity.path)
os.unlink(entity.peek)
return os.unlink(entity.source)
else:
return os.unlink(entity.path, mode)
def link(self, original_path, clone_path):
print('link', original_path, clone_path, 'TODO')
pass
# other:
def mknod(self, path, mode, dev):
return os.mknod(self._get_entity(path).source, mode, dev)
# filesystem
def statfs(self, path):
pass
# symlinks
#def readlink(self, path):
# pass
def symlink(self, destination, symlink_path):
pass
# file # file
def open(self, path, flags): def open(self, path, flags):
return os.open(self._get("open", path), flags) g = self._get("open", path)
print('open:', g)
return os.open(g, flags)
def read(self, path, length, offset, file_handle): def read(self, path, length, offset, file_handle):
os.lseek(file_handle, offset, os.SEEK_SET) os.lseek(file_handle, offset, os.SEEK_SET)
return os.read(file_handle, length) return os.read(file_handle, length)
def _assert_is_writable(self, why, path):
# TODO: might be a bit slow for many repeated writes, but guarantees that
# if a translator is created in src and a file handle was already obtained
# for its output, no further writes can mix things up?
# TODO: write a test for the above test case
entity = self._get_entity("_assert_is_writable for " + why, path)
if entity.is_translator:
raise FuseOSError(errno.EACCES)
else:
return entity
def write(self, path, buffer, offset, file_handle):
self._assert_is_writable("write", path)
os.lseek(file_handle, offset, os.SEEK_SET)
return os.write(file_handle, buffer)
def create(self, path, mode, file = None):
entity = self._assert_is_writable("write", path)
return os.open(entity.path, os.O_CREAT | os.O_WRONLY, mode)
def truncate(self, path, length, file_handle = None):
entity = self._assert_is_writable("truncate", path)
with open(entity.path, 'r+') as f:
f.truncate(length)
def flush(self, path, file_handle):
#self._assert_is_writable("flush", path)
return os.fsync(file_handle)
def release(self, path, file_handle):
print('release', path)
return os.close(file_handle)
def fsync(self, path, fdatasync, file_handle):
self._assert_is_writable("fsync",path)
return self.flush(path, file_handle)
def main(source, cache, mountpoint): def main(source, cache, mountpoint):
FUSE(FilterFS(source, cache), mountpoint, nothreads=True, foreground=True) FUSE(FilterFS(source, cache), mountpoint, nothreads=True, foreground=True)

12
test.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euET -o pipefail
rm test_cache test_source test_actual_result -fr
mkdir -p test_cache test_mnt
cp -ai source test_source
./fs.py test_source test_cache test_mnt
touch test_mnt/touched
echo 42 > test_mnt/written
cp -ai test_mnt test_actual_result
diff -r test_actual_result test_expected_result

View File

@ -0,0 +1,7 @@
output(filename) {
return filename + '.mp3'
}
contents(filename) {
... create an mp3 from the ogg
}

View File

View File

Binary file not shown.

Binary file not shown.

View File

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1 @@
HELLO WORLD!