vfs construction ok

This commit is contained in:
ed 2019-05-29 23:46:17 +00:00
parent 500daacca2
commit 250d0bdf57
6 changed files with 222 additions and 48 deletions

2
.vscode/launch.json vendored
View file

@ -10,7 +10,7 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"args": [ "args": [
"-j", "-j",
"2", "0",
"-nc", "-nc",
"4", "4",
"-nw", "-nw",

View file

@ -1,5 +1,6 @@
{ {
"workbench.colorCustomizations": { "workbench.colorCustomizations": {
// https://ocv.me/dot/bifrost.html
"terminal.background": "#1e1e1e", "terminal.background": "#1e1e1e",
"terminal.foreground": "#d2d2d2", "terminal.foreground": "#d2d2d2",
"terminalCursor.background": "#93A1A1", "terminalCursor.background": "#93A1A1",

View file

@ -7,8 +7,8 @@
turn your phone or raspi into a portable file server with resumable uploads/downloads using IE6 or any other browser turn your phone or raspi into a portable file server with resumable uploads/downloads using IE6 or any other browser
* resumable uploads need `firefox 12+` / `chrome 6+` / `safari 6+` / `IE 10+` * server runs on anything with `py2.7` or `py3.3+`
* server runs on everything with `py2.7` or `py3.3+` * *resumable* uploads need `firefox 12+` / `chrome 6+` / `safari 6+` / `IE 10+`
* code standard: `black` * code standard: `black`
## status ## status

View file

@ -8,6 +8,48 @@ import threading
from .__init__ import * from .__init__ import *
class VFS(object):
"""single level in the virtual fs"""
def __init__(self, realpath, vpath, uread=[], uwrite=[]):
self.realpath = realpath # absolute path on host filesystem
self.vpath = vpath # absolute path in the virtual filesystem
self.uread = uread # users who can read this
self.uwrite = uwrite # users who can write this
self.nodes = {} # child nodes
def add(self, src, dst):
"""get existing, or add new path to the vfs"""
assert not src.endswith("/")
assert not dst.endswith("/")
if "/" in dst:
# requires breadth-first population (permissions trickle down)
name, dst = dst.split("/", 1)
if name in self.nodes:
# exists; do not manipulate permissions
return self.nodes[name].add(src, dst)
n = VFS(
"{}/{}".format(self.realpath, name),
"{}/{}".format(self.vpath, name).lstrip("/"),
self.uread,
self.uwrite,
)
self.nodes[name] = n
return n.add(src, dst)
if dst in self.nodes:
# leaf exists; return as-is
return self.nodes[dst]
# leaf does not exist; create and keep permissions blank
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
n = VFS(src, vp)
self.nodes[dst] = n
return n
class AuthSrv(object): class AuthSrv(object):
"""verifies users against given paths""" """verifies users against given paths"""
@ -28,9 +70,16 @@ class AuthSrv(object):
return {v: k for k, v in orig.items()} return {v: k for k, v in orig.items()}
def reload(self): def reload(self):
"""
construct a flat list of mountpoints and usernames
first from the commandline arguments
then supplementing with config files
before finally building the VFS
"""
user = {} # username:password user = {} # username:password
uread = {} # username:readable-mp mread = {} # mountpoint:[username]
uwrite = {} # username:writable-mp mwrite = {} # mountpoint:[username]
mount = {} # dst:src (mountpoint:realpath) mount = {} # dst:src (mountpoint:realpath)
if self.args.a: if self.args.a:
@ -43,16 +92,19 @@ class AuthSrv(object):
# permset is [rwa]username # permset is [rwa]username
for src, dst, perms in [x.split(":", 2) for x in self.args.v]: for src, dst, perms in [x.split(":", 2) for x in self.args.v]:
src = os.path.abspath(src) src = os.path.abspath(src)
dst = ("/" + dst.strip("/") + "/").replace("//", "/") dst = dst.strip("/")
mount[dst] = src mount[dst] = src
mread[dst] = []
mwrite[dst] = []
perms = perms.split(":") perms = perms.split(":")
for (lvl, uname) in [[x[0], x[1:]] for x in perms]: for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
if uname == "": if uname == "":
uname = "*" uname = "*"
if lvl in "ra": if lvl in "ra":
uread[uname] = dst mread[dst].append(uname)
if lvl in "wa": if lvl in "wa":
uwrite[uname] = dst mwrite[dst].append(uname)
if self.args.c: if self.args.c:
for logfile in self.args.c: for logfile in self.args.c:
@ -61,21 +113,27 @@ class AuthSrv(object):
# self.log(ln) # self.log(ln)
pass pass
with self.mutex: # -h says our defaults are CWD at root and read/write for everyone
self.user = user vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
self.uread = uread
self.uwrite = uwrite
self.mount = mount
self.iuser = self.invert(user)
self.iuread = self.invert(uread)
self.iuwrite = self.invert(uwrite)
self.imount = self.invert(mount)
pprint.pprint( maxdepth = 0
{ for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
"user": self.user, depth = dst.count("/")
"uread": self.uread, assert maxdepth <= depth
"uwrite": self.uwrite, maxdepth = depth
"mount": self.mount,
} if dst == "":
) # rootfs was mapped; fully replaces the default CWD vfs
vfs = VFS(mount[dst], dst, mread[dst], mwrite[dst])
continue
v = vfs.add(mount[dst], dst)
v.uread = mread[dst]
v.uwrite = mwrite[dst]
with self.mutex:
self.vfs = vfs
self.user = user
self.iuser = self.invert(user)
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})

View file

@ -1,38 +1,41 @@
# any line with a : creates a user, # create users:
# username:password # u username:password
# so you can create users anywhere really u ed:123
# but keeping them here is prob a good idea u k:k
ed:123
k:k
# leave a blank line before each volume # leave a blank line between volumes
# (and also between users and volumes)
# this is a volume, # create a volume:
# it shares the contents of /home/... # share "." (the current directory)
# and appears at "/dj" in the web-ui # as "/" (the webroot) for the following users:
# "r" grants read-access for anyone # "r" grants read-access for anyone
# "a ed" grants read-write to ed # "a ed" grants read-write to ed
/home/ed/Music/dj .
/dj /
r r
a ed a ed
# display /home/ed/ocv.me as the webroot # custom permissions for the "priv" folder:
# and allow user "k" to see/read it # user "k" can see/read the contents
/home/ed/ocv.me # and "ed" gets read-write access
/ ./priv
/priv
r k r k
a ed
# this shares the current directory as "/pwd" # share /home/ed/Music/ as /music and let anyone read it
# but does nothing since there's no permissions # (this will replace any folder called "music" in the webroot)
. /home/ed/Music
/pwd /music
r
# and a folder where anyone can upload # and a folder where anyone can upload
# but nobody can see the contents # but nobody can see the contents
/home/ed/inc /home/ed/inc
/incoming /dump
w w
# you can use relative paths too btw # this entire config file can be replaced with these arguments:
# but they're a pain for testing purpose so I didn't # -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w
# but note that the config file always wins in case of conflicts

112
tests/test_vfs.py Normal file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env python
# coding: utf-8
from __future__ import print_function
import os
import json
import shutil
import unittest
from argparse import Namespace
from copyparty.authsrv import *
class TestVFS(unittest.TestCase):
def dump(self, vfs):
print(json.dumps(vfs, indent=4, sort_keys=True, default=lambda o: o.__dict__))
def test(self):
td = "/dev/shm/vfs"
try:
shutil.rmtree(td)
except:
pass
os.mkdir(td)
os.chdir(td)
for a in "abc":
for b in "abc":
for c in "abc":
folder = "{0}/{0}{1}/{0}{1}{2}".format(a, b, c)
os.makedirs(folder)
for d in "abc":
fn = "{}/{}{}{}{}".format(folder, a, b, c, d)
with open(fn, "w") as f:
f.write(fn)
# defaults
vfs = AuthSrv(Namespace(c=None, a=[], v=[]), None).vfs
self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, ["*"])
# single read-only rootfs (relative path)
vfs = AuthSrv(Namespace(c=None, a=[], v=["a/ab/::r"]), None).vfs
self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td + "/a/ab")
self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, [])
# single read-only rootfs (absolute path)
vfs = AuthSrv(Namespace(c=None, a=[], v=[td + "//a/ac/../aa//::r"]), None).vfs
self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td + "/a/aa")
self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, [])
# read-only rootfs with write-only subdirectory
vfs = AuthSrv(
Namespace(c=None, a=[], v=[".::r", "a/ac/acb:a/ac/acb:w"]), None
).vfs
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, [])
n = vfs.nodes["a"]
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, td + "/a")
self.assertEqual(n.uread, ["*"])
self.assertEqual(n.uwrite, [])
n = n.nodes["ac"]
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a/ac")
self.assertEqual(n.realpath, td + "/a/ac")
self.assertEqual(n.uread, ["*"])
self.assertEqual(n.uwrite, [])
n = n.nodes["acb"]
self.assertEqual(n.nodes, {})
self.assertEqual(n.vpath, "a/ac/acb")
self.assertEqual(n.realpath, td + "/a/ac/acb")
self.assertEqual(n.uread, [])
self.assertEqual(n.uwrite, ["*"])
# breadth-first construction
vfs = AuthSrv(
Namespace(
c=None,
a=[],
v=[
"a/ac/acb:a/ac/acb:w",
"a:a:w",
".::r",
"abacdfasdq:abacdfasdq:w",
"a/ac:a/ac:w",
],
),
None,
).vfs
# shadowing
# crossreferences
# loops
# listdir mapping
# access reduction
shutil.rmtree(td)