mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
vfs construction ok
This commit is contained in:
parent
500daacca2
commit
250d0bdf57
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
@ -10,7 +10,7 @@
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"args": [
|
"args": [
|
||||||
"-j",
|
"-j",
|
||||||
"2",
|
"0",
|
||||||
"-nc",
|
"-nc",
|
||||||
"4",
|
"4",
|
||||||
"-nw",
|
"-nw",
|
||||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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
112
tests/test_vfs.py
Normal 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)
|
Loading…
Reference in a new issue