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/
/mnt/
/test_cache
/test_mnt
/test_source
/test_actual_result

154
fs.py
View File

@ -1,10 +1,12 @@
#!/usr/bin/env python
import os, sys, shutil, pathlib
import os, sys, errno, shutil, pathlib
from fuse import FUSE, FuseOSError, Operations
from collections import namedtuple
Translator = namedtuple('Translator', ['mode', 'output_filename', 'command'])
# todo: rename 'path' to 'get'
Entity = namedtuple('Entity', ['path', 'is_translator', 'peek', 'source', 'size'])
class FilterFS(Operations):
def __init__(self, source, cache):
@ -23,23 +25,6 @@ class FilterFS(Operations):
output_filename = components[1], # TODO: forbid ../ attacks
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):
print('get', why, path)
@ -61,6 +46,28 @@ class FilterFS(Operations):
# end hack
p = cached_output
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
def readdir(self, path, fh):
@ -70,29 +77,118 @@ class FilterFS(Operations):
if os.path.isdir(orig):
for d in os.listdir(orig):
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):
st = os.lstat(self._peek("getattr", path))
entity = self._get_entity("getattr", path)
attr = os.lstat(entity.path)
print(entity.path)
return {
'st_mode': st.st_mode, # 0o100775 file, 0o40775 dir
'st_mode': attr.st_mode, # 0o100775 file, 0o40775 dir
#'st_ino': 42,
#'st_dev': 123,
'st_nlink': st.st_nlink,
'st_uid': st.st_uid,
'st_gid': st.st_gid,
'st_size': 999999999999999, #st.st_size, # TODO: max file size
'st_atime': st.st_atime,
'st_mtime': st.st_mtime,
'st_ctime': st.st_ctime,
'st_nlink': attr.st_nlink,
'st_uid': attr.st_uid,
'st_gid': attr.st_gid,
'st_size': attr.st_size if entity.size is None else entity.size,
'st_atime': attr.st_atime,
'st_mtime': attr.st_mtime,
'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
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):
os.lseek(file_handle, offset, os.SEEK_SET)
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):
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!