test
This commit is contained in:
parent
7f6d86d8db
commit
4aa92683e0
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
||||||
/cache/
|
/cache/
|
||||||
/mnt/
|
/mnt/
|
||||||
|
/test_cache
|
||||||
|
/test_mnt
|
||||||
|
/test_source
|
||||||
|
/test_actual_result
|
||||||
|
|
154
fs.py
154
fs.py
|
@ -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
12
test.sh
Executable 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
|
7
test_expected_result/*.ogg
Normal file
7
test_expected_result/*.ogg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
output(filename) {
|
||||||
|
return filename + '.mp3'
|
||||||
|
}
|
||||||
|
|
||||||
|
contents(filename) {
|
||||||
|
... create an mp3 from the ogg
|
||||||
|
}
|
0
test_expected_result/bar.ogg
Normal file
0
test_expected_result/bar.ogg
Normal file
0
test_expected_result/baz.ogg
Normal file
0
test_expected_result/baz.ogg
Normal file
BIN
test_expected_result/foo.mp3
Normal file
BIN
test_expected_result/foo.mp3
Normal file
Binary file not shown.
BIN
test_expected_result/foo.ogg
Normal file
BIN
test_expected_result/foo.ogg
Normal file
Binary file not shown.
0
test_expected_result/quux.ogg
Normal file
0
test_expected_result/quux.ogg
Normal file
1
test_expected_result/some_dir/aaa
Normal file
1
test_expected_result/some_dir/aaa
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Hello World!
|
1
test_expected_result/some_dir/bbb
Normal file
1
test_expected_result/some_dir/bbb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
HELLO WORLD!
|
Loading…
Reference in New Issue
Block a user