test
This commit is contained in:
parent
7f6d86d8db
commit
4aa92683e0
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
|||
/cache/
|
||||
/mnt/
|
||||
/test_cache
|
||||
/test_mnt
|
||||
/test_source
|
||||
/test_actual_result
|
||||
|
|
154
fs.py
154
fs.py
|
@ -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
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