mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
1039 lines
33 KiB
Python
1039 lines
33 KiB
Python
# coding: utf-8
|
|
from __future__ import print_function, unicode_literals
|
|
|
|
import re
|
|
import os
|
|
import sys
|
|
import stat
|
|
import time
|
|
import base64
|
|
import hashlib
|
|
import threading
|
|
from datetime import datetime
|
|
|
|
from .__init__ import WINDOWS
|
|
from .util import (
|
|
IMPLICATIONS,
|
|
uncyg,
|
|
undot,
|
|
unhumanize,
|
|
absreal,
|
|
Pebkac,
|
|
fsenc,
|
|
statdir,
|
|
)
|
|
from .bos import bos
|
|
|
|
|
|
class AXS(object):
|
|
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
|
self.uread = {} if uread is None else {k: 1 for k in uread}
|
|
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
|
|
self.umove = {} if umove is None else {k: 1 for k in umove}
|
|
self.udel = {} if udel is None else {k: 1 for k in udel}
|
|
|
|
def __repr__(self):
|
|
return "AXS({})".format(
|
|
", ".join(
|
|
"{}={!r}".format(k, self.__dict__[k])
|
|
for k in "uread uwrite umove udel".split()
|
|
)
|
|
)
|
|
|
|
|
|
class Lim(object):
|
|
def __init__(self):
|
|
self.nups = {} # num tracker
|
|
self.bups = {} # byte tracker list
|
|
self.bupc = {} # byte tracker cache
|
|
|
|
self.nosub = False # disallow subdirectories
|
|
|
|
self.smin = None # filesize min
|
|
self.smax = None # filesize max
|
|
|
|
self.bwin = None # bytes window
|
|
self.bmax = None # bytes max
|
|
self.nwin = None # num window
|
|
self.nmax = None # num max
|
|
|
|
self.rotn = None # rot num files
|
|
self.rotl = None # rot depth
|
|
self.rotf = None # rot datefmt
|
|
self.rot_re = None # rotf check
|
|
|
|
def set_rotf(self, fmt):
|
|
self.rotf = fmt
|
|
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
|
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
|
self.rot_re = re.compile("(^|/)" + r + "$")
|
|
|
|
def all(self, ip, rem, sz, abspath):
|
|
self.chk_nup(ip)
|
|
self.chk_bup(ip)
|
|
self.chk_rem(rem)
|
|
if sz != -1:
|
|
self.chk_sz(sz)
|
|
|
|
ap2, vp2 = self.rot(abspath)
|
|
if abspath == ap2:
|
|
return ap2, rem
|
|
|
|
return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)
|
|
|
|
def chk_sz(self, sz):
|
|
if self.smin is not None and sz < self.smin:
|
|
raise Pebkac(400, "file too small")
|
|
|
|
if self.smax is not None and sz > self.smax:
|
|
raise Pebkac(400, "file too big")
|
|
|
|
def chk_rem(self, rem):
|
|
if self.nosub and rem:
|
|
raise Pebkac(500, "no subdirectories allowed")
|
|
|
|
def rot(self, path):
|
|
if not self.rotf and not self.rotn:
|
|
return path, ""
|
|
|
|
if self.rotf:
|
|
path = path.rstrip("/\\")
|
|
if self.rot_re.search(path.replace("\\", "/")):
|
|
return path, ""
|
|
|
|
suf = datetime.utcnow().strftime(self.rotf)
|
|
if path:
|
|
path += "/"
|
|
|
|
return path + suf, suf
|
|
|
|
ret = self.dive(path, self.rotl)
|
|
if not ret:
|
|
raise Pebkac(500, "no available slots in volume")
|
|
|
|
d = ret[len(path) :].strip("/\\").replace("\\", "/")
|
|
return ret, d
|
|
|
|
def dive(self, path, lvs):
|
|
items = bos.listdir(path)
|
|
|
|
if not lvs:
|
|
# at leaf level
|
|
return None if len(items) >= self.rotn else ""
|
|
|
|
dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
|
|
dirs.sort()
|
|
|
|
if not dirs:
|
|
# no branches yet; make one
|
|
sub = os.path.join(path, "0")
|
|
bos.mkdir(sub)
|
|
else:
|
|
# try newest branch only
|
|
sub = os.path.join(path, str(dirs[-1]))
|
|
|
|
ret = self.dive(sub, lvs - 1)
|
|
if ret is not None:
|
|
return os.path.join(sub, ret)
|
|
|
|
if len(dirs) >= self.rotn:
|
|
# full branch or root
|
|
return None
|
|
|
|
# make a branch
|
|
sub = os.path.join(path, str(dirs[-1] + 1))
|
|
bos.mkdir(sub)
|
|
ret = self.dive(sub, lvs - 1)
|
|
if ret is None:
|
|
raise Pebkac(500, "rotation bug")
|
|
|
|
return os.path.join(sub, ret)
|
|
|
|
def nup(self, ip):
|
|
try:
|
|
self.nups[ip].append(time.time())
|
|
except:
|
|
self.nups[ip] = [time.time()]
|
|
|
|
def bup(self, ip, nbytes):
|
|
v = [time.time(), nbytes]
|
|
try:
|
|
self.bups[ip].append(v)
|
|
self.bupc[ip] += nbytes
|
|
except:
|
|
self.bups[ip] = [v]
|
|
self.bupc[ip] = nbytes
|
|
|
|
def chk_nup(self, ip):
|
|
if not self.nmax or ip not in self.nups:
|
|
return
|
|
|
|
nups = self.nups[ip]
|
|
cutoff = time.time() - self.nwin
|
|
while nups and nups[0] < cutoff:
|
|
nups.pop(0)
|
|
|
|
if len(nups) >= self.nmax:
|
|
raise Pebkac(429, "too many uploads")
|
|
|
|
def chk_bup(self, ip):
|
|
if not self.bmax or ip not in self.bups:
|
|
return
|
|
|
|
bups = self.bups[ip]
|
|
cutoff = time.time() - self.bwin
|
|
mark = self.bupc[ip]
|
|
while bups and bups[0][0] < cutoff:
|
|
mark -= bups.pop(0)[1]
|
|
|
|
self.bupc[ip] = mark
|
|
if mark >= self.bmax:
|
|
raise Pebkac(429, "ingress saturated")
|
|
|
|
|
|
class VFS(object):
|
|
"""single level in the virtual fs"""
|
|
|
|
def __init__(self, log, realpath, vpath, axs, flags):
|
|
self.log = log
|
|
self.realpath = realpath # absolute path on host filesystem
|
|
self.vpath = vpath # absolute path in the virtual filesystem
|
|
self.axs = axs # type: AXS
|
|
self.flags = flags # config options
|
|
self.nodes = {} # child nodes
|
|
self.histtab = None # all realpath->histpath
|
|
self.dbv = None # closest full/non-jump parent
|
|
self.lim = None # type: Lim # upload limits; only set for dbv
|
|
|
|
if realpath:
|
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
|
self.all_vols = {vpath: self} # flattened recursive
|
|
self.aread = {}
|
|
self.awrite = {}
|
|
self.amove = {}
|
|
self.adel = {}
|
|
else:
|
|
self.histpath = None
|
|
self.all_vols = None
|
|
self.aread = None
|
|
self.awrite = None
|
|
self.amove = None
|
|
self.adel = None
|
|
|
|
def __repr__(self):
|
|
return "VFS({})".format(
|
|
", ".join(
|
|
"{}={!r}".format(k, self.__dict__[k])
|
|
for k in "realpath vpath axs flags".split()
|
|
)
|
|
)
|
|
|
|
def get_all_vols(self, outdict):
|
|
if self.realpath:
|
|
outdict[self.vpath] = self
|
|
|
|
for v in self.nodes.values():
|
|
v.get_all_vols(outdict)
|
|
|
|
def add(self, src, dst):
|
|
"""get existing, or add new path to the vfs"""
|
|
assert not src.endswith("/") # nosec
|
|
assert not dst.endswith("/") # nosec
|
|
|
|
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)
|
|
|
|
vn = VFS(
|
|
self.log,
|
|
os.path.join(self.realpath, name) if self.realpath else None,
|
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
|
self.axs,
|
|
self._copy_flags(name),
|
|
)
|
|
vn.dbv = self.dbv or self
|
|
self.nodes[name] = vn
|
|
return vn.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("/")
|
|
vn = VFS(self.log, src, vp, AXS(), {})
|
|
vn.dbv = self.dbv or self
|
|
self.nodes[dst] = vn
|
|
return vn
|
|
|
|
def _copy_flags(self, name):
|
|
flags = {k: v for k, v in self.flags.items()}
|
|
hist = flags.get("hist")
|
|
if hist and hist != "-":
|
|
flags["hist"] = "{}/{}".format(hist.rstrip("/"), name)
|
|
|
|
return flags
|
|
|
|
def bubble_flags(self):
|
|
if self.dbv:
|
|
for k, v in self.dbv.flags.items():
|
|
if k not in ["hist"]:
|
|
self.flags[k] = v
|
|
|
|
for v in self.nodes.values():
|
|
v.bubble_flags()
|
|
|
|
def _find(self, vpath):
|
|
"""return [vfs,remainder]"""
|
|
vpath = undot(vpath)
|
|
if vpath == "":
|
|
return [self, ""]
|
|
|
|
if "/" in vpath:
|
|
name, rem = vpath.split("/", 1)
|
|
else:
|
|
name = vpath
|
|
rem = ""
|
|
|
|
if name in self.nodes:
|
|
return self.nodes[name]._find(rem)
|
|
|
|
return [self, vpath]
|
|
|
|
def can_access(self, vpath, uname):
|
|
# type: (str, str) -> tuple[bool, bool, bool, bool]
|
|
"""can Read,Write,Move,Delete"""
|
|
vn, _ = self._find(vpath)
|
|
c = vn.axs
|
|
return [
|
|
uname in c.uread or "*" in c.uread,
|
|
uname in c.uwrite or "*" in c.uwrite,
|
|
uname in c.umove or "*" in c.umove,
|
|
uname in c.udel or "*" in c.udel,
|
|
]
|
|
|
|
def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
|
|
# type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str]
|
|
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
|
vn, rem = self._find(vpath)
|
|
c = vn.axs
|
|
|
|
for req, d, msg in [
|
|
[will_read, c.uread, "read"],
|
|
[will_write, c.uwrite, "write"],
|
|
[will_move, c.umove, "move"],
|
|
[will_del, c.udel, "delete"],
|
|
]:
|
|
if req and (uname not in d and "*" not in d):
|
|
m = "you don't have {}-access for this location"
|
|
raise Pebkac(403, m.format(msg))
|
|
|
|
return vn, rem
|
|
|
|
def get_dbv(self, vrem):
|
|
# type: (str) -> tuple[VFS, str]
|
|
dbv = self.dbv
|
|
if not dbv:
|
|
return self, vrem
|
|
|
|
vrem = [self.vpath[len(dbv.vpath) + 1 :], vrem]
|
|
vrem = "/".join([x for x in vrem if x])
|
|
return dbv, vrem
|
|
|
|
def canonical(self, rem, resolve=True):
|
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
|
rp = self.realpath
|
|
if rem:
|
|
rp += "/" + rem
|
|
|
|
return absreal(rp) if resolve else rp
|
|
|
|
def ls(self, rem, uname, scandir, permsets, lstat=False):
|
|
# type: (str, str, bool, list[list[bool]], bool) -> tuple[str, str, dict[str, VFS]]
|
|
"""return user-readable [fsdir,real,virt] items at vpath"""
|
|
virt_vis = {} # nodes readable by user
|
|
abspath = self.canonical(rem)
|
|
real = list(statdir(self.log, scandir, lstat, abspath))
|
|
real.sort()
|
|
if not rem:
|
|
# no vfs nodes in the list of real inodes
|
|
real = [x for x in real if x[0] not in self.nodes]
|
|
|
|
for name, vn2 in sorted(self.nodes.items()):
|
|
ok = False
|
|
axs = vn2.axs
|
|
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel]
|
|
for pset in permsets:
|
|
ok = True
|
|
for req, lst in zip(pset, axs):
|
|
if req and uname not in lst and "*" not in lst:
|
|
ok = False
|
|
if ok:
|
|
break
|
|
|
|
if ok:
|
|
virt_vis[name] = vn2
|
|
|
|
return [abspath, real, virt_vis]
|
|
|
|
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
|
|
"""
|
|
recursively yields from ./rem;
|
|
rel is a unix-style user-defined vpath (not vfs-related)
|
|
"""
|
|
|
|
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
|
|
dbv, vrem = self.get_dbv(rem)
|
|
|
|
if (
|
|
seen
|
|
and (not fsroot.startswith(seen[-1]) or fsroot == seen[-1])
|
|
and fsroot in seen
|
|
):
|
|
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}/{}"
|
|
self.log("vfs.walk", m.format(seen[-1], fsroot, self.vpath, rem), 3)
|
|
return
|
|
|
|
seen = seen[:] + [fsroot]
|
|
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
|
|
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
|
|
|
|
rfiles.sort()
|
|
rdirs.sort()
|
|
|
|
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
|
|
|
|
for rdir, _ in rdirs:
|
|
if not dots and rdir.startswith("."):
|
|
continue
|
|
|
|
wrel = (rel + "/" + rdir).lstrip("/")
|
|
wrem = (rem + "/" + rdir).lstrip("/")
|
|
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
|
|
yield x
|
|
|
|
for n, vfs in sorted(vfs_virt.items()):
|
|
if not dots and n.startswith("."):
|
|
continue
|
|
|
|
wrel = (rel + "/" + n).lstrip("/")
|
|
for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat):
|
|
yield x
|
|
|
|
def zipgen(self, vrem, flt, uname, dots, scandir):
|
|
if flt:
|
|
flt = {k: True for k in flt}
|
|
|
|
f1 = "{0}.hist{0}up2k.".format(os.sep)
|
|
f2a = os.sep + "dir.txt"
|
|
f2b = "{0}.hist{0}".format(os.sep)
|
|
|
|
g = self.walk("", vrem, [], uname, [[True]], dots, scandir, False)
|
|
for _, _, vpath, apath, files, rd, vd in g:
|
|
if flt:
|
|
files = [x for x in files if x[0] in flt]
|
|
|
|
rm = [x for x in rd if x[0] not in flt]
|
|
[rd.remove(x) for x in rm]
|
|
|
|
rm = [x for x in vd.keys() if x not in flt]
|
|
[vd.pop(x) for x in rm]
|
|
|
|
flt = None
|
|
|
|
# print(repr([vpath, apath, [x[0] for x in files]]))
|
|
fnames = [n[0] for n in files]
|
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
|
apaths = [os.path.join(apath, n) for n in fnames]
|
|
files = list(zip(vpaths, apaths, files))
|
|
|
|
if not dots:
|
|
# dotfile filtering based on vpath (intended visibility)
|
|
files = [x for x in files if "/." not in "/" + x[0]]
|
|
|
|
rm = [x for x in rd if x[0].startswith(".")]
|
|
for x in rm:
|
|
rd.remove(x)
|
|
|
|
rm = [k for k in vd.keys() if k.startswith(".")]
|
|
for x in rm:
|
|
del vd[x]
|
|
|
|
# up2k filetring based on actual abspath
|
|
files = [
|
|
x
|
|
for x in files
|
|
if f1 not in x[1] and (not x[1].endswith(f2a) or f2b not in x[1])
|
|
]
|
|
|
|
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
|
yield f
|
|
|
|
|
|
if WINDOWS:
|
|
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
|
else:
|
|
re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
|
|
|
|
|
class AuthSrv(object):
|
|
"""verifies users against given paths"""
|
|
|
|
def __init__(self, args, log_func, warn_anonwrite=True):
|
|
self.args = args
|
|
self.log_func = log_func
|
|
self.warn_anonwrite = warn_anonwrite
|
|
self.line_ctr = 0
|
|
|
|
self.mutex = threading.Lock()
|
|
self.reload()
|
|
|
|
def log(self, msg, c=0):
|
|
if self.log_func:
|
|
self.log_func("auth", msg, c)
|
|
|
|
def laggy_iter(self, iterable):
|
|
"""returns [value,isFinalValue]"""
|
|
it = iter(iterable)
|
|
prev = next(it)
|
|
for x in it:
|
|
yield prev, False
|
|
prev = x
|
|
|
|
yield prev, True
|
|
|
|
def _parse_config_file(self, fd, acct, daxs, mflags, mount):
|
|
# type: (any, str, dict[str, AXS], any, str) -> None
|
|
vol_src = None
|
|
vol_dst = None
|
|
self.line_ctr = 0
|
|
for ln in [x.decode("utf-8").strip() for x in fd]:
|
|
self.line_ctr += 1
|
|
if not ln and vol_src is not None:
|
|
vol_src = None
|
|
vol_dst = None
|
|
|
|
if not ln or ln.startswith("#"):
|
|
continue
|
|
|
|
if vol_src is None:
|
|
if ln.startswith("u "):
|
|
u, p = ln[2:].split(":", 1)
|
|
acct[u] = p
|
|
else:
|
|
vol_src = ln
|
|
continue
|
|
|
|
if vol_src and vol_dst is None:
|
|
vol_dst = ln
|
|
if not vol_dst.startswith("/"):
|
|
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
|
|
|
# cfg files override arguments and previous files
|
|
vol_src = bos.path.abspath(vol_src)
|
|
vol_dst = vol_dst.strip("/")
|
|
mount[vol_dst] = vol_src
|
|
daxs[vol_dst] = AXS()
|
|
mflags[vol_dst] = {}
|
|
continue
|
|
|
|
try:
|
|
lvl, uname = ln.split(" ", 1)
|
|
except:
|
|
lvl = ln
|
|
uname = "*"
|
|
|
|
if lvl == "a":
|
|
m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
|
|
self.log(m, 1)
|
|
|
|
self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
|
|
|
|
def _read_vol_str(self, lvl, uname, axs, flags):
|
|
# type: (str, str, AXS, any) -> None
|
|
if lvl == "c":
|
|
cval = True
|
|
if "=" in uname:
|
|
uname, cval = uname.split("=", 1)
|
|
|
|
self._read_volflag(flags, uname, cval, False)
|
|
return
|
|
|
|
if uname == "":
|
|
uname = "*"
|
|
|
|
for un in uname.split(","):
|
|
if "r" in lvl:
|
|
axs.uread[un] = 1
|
|
|
|
if "w" in lvl:
|
|
axs.uwrite[un] = 1
|
|
|
|
if "m" in lvl:
|
|
axs.umove[un] = 1
|
|
|
|
if "d" in lvl:
|
|
axs.udel[un] = 1
|
|
|
|
def _read_volflag(self, flags, name, value, is_list):
|
|
if name not in ["mtp"]:
|
|
flags[name] = value
|
|
return
|
|
|
|
if not is_list:
|
|
value = [value]
|
|
elif not value:
|
|
return
|
|
|
|
flags[name] = flags.get(name, []) + value
|
|
|
|
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
|
|
"""
|
|
|
|
acct = {} # username:password
|
|
daxs = {} # type: dict[str, AXS]
|
|
mflags = {} # mountpoint:[flag]
|
|
mount = {} # dst:src (mountpoint:realpath)
|
|
|
|
if self.args.a:
|
|
# list of username:password
|
|
for x in self.args.a:
|
|
try:
|
|
u, p = x.split(":", 1)
|
|
acct[u] = p
|
|
except:
|
|
m = '\n invalid value "{}" for argument -a, must be username:password'
|
|
raise Exception(m.format(x))
|
|
|
|
if self.args.v:
|
|
# list of src:dst:permset:permset:...
|
|
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
|
for v_str in self.args.v:
|
|
m = re_vol.match(v_str)
|
|
if not m:
|
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
|
|
|
src, dst, perms = m.groups()
|
|
if WINDOWS:
|
|
src = uncyg(src)
|
|
|
|
# print("\n".join([src, dst, perms]))
|
|
src = bos.path.abspath(src)
|
|
dst = dst.strip("/")
|
|
mount[dst] = src
|
|
daxs[dst] = AXS()
|
|
mflags[dst] = {}
|
|
|
|
for x in perms.split(":"):
|
|
lvl, uname = x.split(",", 1) if "," in x else [x, ""]
|
|
self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
|
|
|
|
if self.args.c:
|
|
for cfg_fn in self.args.c:
|
|
with open(cfg_fn, "rb") as f:
|
|
try:
|
|
self._parse_config_file(f, acct, daxs, mflags, mount)
|
|
except:
|
|
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
|
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
|
raise
|
|
|
|
# case-insensitive; normalize
|
|
if WINDOWS:
|
|
cased = {}
|
|
for k, v in mount.items():
|
|
cased[k] = absreal(v)
|
|
|
|
mount = cased
|
|
|
|
if not mount:
|
|
# -h says our defaults are CWD at root and read/write for everyone
|
|
axs = AXS(["*"], ["*"], None, None)
|
|
vfs = VFS(self.log_func, bos.path.abspath("."), "", axs, {})
|
|
elif "" not in mount:
|
|
# there's volumes but no root; make root inaccessible
|
|
vfs = VFS(self.log_func, None, "", AXS(), {})
|
|
vfs.flags["d2d"] = True
|
|
|
|
maxdepth = 0
|
|
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
|
depth = dst.count("/")
|
|
assert maxdepth <= depth # nosec
|
|
maxdepth = depth
|
|
|
|
if dst == "":
|
|
# rootfs was mapped; fully replaces the default CWD vfs
|
|
vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
|
|
continue
|
|
|
|
v = vfs.add(mount[dst], dst)
|
|
v.axs = daxs[dst]
|
|
v.flags = mflags[dst]
|
|
v.dbv = None
|
|
|
|
vfs.all_vols = {}
|
|
vfs.get_all_vols(vfs.all_vols)
|
|
|
|
for perm in "read write move del".split():
|
|
axs_key = "u" + perm
|
|
unames = ["*"] + list(acct.keys())
|
|
umap = {x: [] for x in unames}
|
|
for usr in unames:
|
|
for mp, vol in vfs.all_vols.items():
|
|
if usr in getattr(vol.axs, axs_key):
|
|
umap[usr].append(mp)
|
|
setattr(vfs, "a" + perm, umap)
|
|
|
|
all_users = {}
|
|
missing_users = {}
|
|
for axs in daxs.values():
|
|
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
|
|
for usr in d.keys():
|
|
all_users[usr] = 1
|
|
if usr != "*" and usr not in acct:
|
|
missing_users[usr] = 1
|
|
|
|
if missing_users:
|
|
self.log(
|
|
"you must -a the following users: "
|
|
+ ", ".join(k for k in sorted(missing_users)),
|
|
c=1,
|
|
)
|
|
raise Exception("invalid config")
|
|
|
|
promote = []
|
|
demote = []
|
|
for vol in vfs.all_vols.values():
|
|
hid = hashlib.sha512(fsenc(vol.realpath)).digest()
|
|
hid = base64.b32encode(hid).decode("ascii").lower()
|
|
vflag = vol.flags.get("hist")
|
|
if vflag == "-":
|
|
pass
|
|
elif vflag:
|
|
vol.histpath = uncyg(vflag) if WINDOWS else vflag
|
|
elif self.args.hist:
|
|
for nch in range(len(hid)):
|
|
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
|
bos.makedirs(hpath)
|
|
|
|
powner = os.path.join(hpath, "owner.txt")
|
|
try:
|
|
with open(powner, "rb") as f:
|
|
owner = f.read().rstrip()
|
|
except:
|
|
owner = None
|
|
|
|
me = fsenc(vol.realpath).rstrip()
|
|
if owner not in [None, me]:
|
|
continue
|
|
|
|
if owner is None:
|
|
with open(powner, "wb") as f:
|
|
f.write(me)
|
|
|
|
vol.histpath = hpath
|
|
break
|
|
|
|
vol.histpath = absreal(vol.histpath)
|
|
if vol.dbv:
|
|
if bos.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
|
promote.append(vol)
|
|
vol.dbv = None
|
|
else:
|
|
demote.append(vol)
|
|
|
|
# discard jump-vols
|
|
for v in demote:
|
|
vfs.all_vols.pop(v.vpath)
|
|
|
|
if promote:
|
|
msg = [
|
|
"\n the following jump-volumes were generated to assist the vfs.\n As they contain a database (probably from v0.11.11 or older),\n they are promoted to full volumes:"
|
|
]
|
|
for vol in promote:
|
|
msg.append(
|
|
" /{} ({}) ({})".format(vol.vpath, vol.realpath, vol.histpath)
|
|
)
|
|
|
|
self.log("\n\n".join(msg) + "\n", c=3)
|
|
|
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
|
|
|
for vol in vfs.all_vols.values():
|
|
lim = Lim()
|
|
use = False
|
|
|
|
if vol.flags.get("nosub"):
|
|
use = True
|
|
lim.nosub = True
|
|
|
|
v = vol.flags.get("sz")
|
|
if v:
|
|
use = True
|
|
lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]
|
|
|
|
v = vol.flags.get("rotn")
|
|
if v:
|
|
use = True
|
|
lim.rotn, lim.rotl = [int(x) for x in v.split(",")]
|
|
|
|
v = vol.flags.get("rotf")
|
|
if v:
|
|
use = True
|
|
lim.set_rotf(v)
|
|
|
|
v = vol.flags.get("maxn")
|
|
if v:
|
|
use = True
|
|
lim.nmax, lim.nwin = [int(x) for x in v.split(",")]
|
|
|
|
v = vol.flags.get("maxb")
|
|
if v:
|
|
use = True
|
|
lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]
|
|
|
|
if use:
|
|
vol.lim = lim
|
|
|
|
for vol in vfs.all_vols.values():
|
|
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
|
vol.flags["gz"] = False # def.pk
|
|
|
|
if "scan" in vol.flags:
|
|
vol.flags["scan"] = int(vol.flags["scan"])
|
|
elif self.args.re_maxage:
|
|
vol.flags["scan"] = self.args.re_maxage
|
|
|
|
all_mte = {}
|
|
errors = False
|
|
for vol in vfs.all_vols.values():
|
|
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
|
vol.flags["e2ds"] = True
|
|
|
|
if self.args.e2d or "e2ds" in vol.flags:
|
|
vol.flags["e2d"] = True
|
|
|
|
if self.args.no_hash:
|
|
if "ehash" not in vol.flags:
|
|
vol.flags["dhash"] = True
|
|
|
|
for k in ["e2t", "e2ts", "e2tsr"]:
|
|
if getattr(self.args, k):
|
|
vol.flags[k] = True
|
|
|
|
for k1, k2 in IMPLICATIONS:
|
|
if k1 in vol.flags:
|
|
vol.flags[k2] = True
|
|
|
|
# default tag cfgs if unset
|
|
if "mte" not in vol.flags:
|
|
vol.flags["mte"] = self.args.mte
|
|
if "mth" not in vol.flags:
|
|
vol.flags["mth"] = self.args.mth
|
|
|
|
# append parsers from argv to volume-flags
|
|
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
|
|
|
# d2d drops all database features for a volume
|
|
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"]]:
|
|
if not vol.flags.get(grp, False):
|
|
continue
|
|
|
|
vol.flags["d2t"] = True
|
|
vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}
|
|
|
|
# mt* needs e2t so drop those too
|
|
for grp, rm in [["e2t", "mt"]]:
|
|
if vol.flags.get(grp, False):
|
|
continue
|
|
|
|
vol.flags = {k: v for k, v in vol.flags.items() if not k.startswith(rm)}
|
|
|
|
# verify tags mentioned by -mt[mp] are used by -mte
|
|
local_mtp = {}
|
|
local_only_mtp = {}
|
|
tags = vol.flags.get("mtp", []) + vol.flags.get("mtm", [])
|
|
tags = [x.split("=")[0] for x in tags]
|
|
tags = [y for x in tags for y in x.split(",")]
|
|
for a in tags:
|
|
local_mtp[a] = True
|
|
local = True
|
|
for b in self.args.mtp or []:
|
|
b = b.split("=")[0]
|
|
if a == b:
|
|
local = False
|
|
|
|
if local:
|
|
local_only_mtp[a] = True
|
|
|
|
local_mte = {}
|
|
for a in vol.flags.get("mte", "").split(","):
|
|
local = True
|
|
all_mte[a] = True
|
|
local_mte[a] = True
|
|
for b in self.args.mte.split(","):
|
|
if not a or not b:
|
|
continue
|
|
|
|
if a == b:
|
|
local = False
|
|
|
|
for mtp in local_only_mtp.keys():
|
|
if mtp not in local_mte:
|
|
m = 'volume "/{}" defines metadata tag "{}", but doesnt use it in "-mte" (or with "cmte" in its volume-flags)'
|
|
self.log(m.format(vol.vpath, mtp), 1)
|
|
errors = True
|
|
|
|
tags = self.args.mtp or []
|
|
tags = [x.split("=")[0] for x in tags]
|
|
tags = [y for x in tags for y in x.split(",")]
|
|
for mtp in tags:
|
|
if mtp not in all_mte:
|
|
m = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volume-flag)'
|
|
self.log(m.format(mtp), 1)
|
|
errors = True
|
|
|
|
if errors:
|
|
sys.exit(1)
|
|
|
|
vfs.bubble_flags()
|
|
|
|
m = "volumes and permissions:\n"
|
|
for v in vfs.all_vols.values():
|
|
if not self.warn_anonwrite:
|
|
break
|
|
|
|
m += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(v.vpath, v.realpath)
|
|
for txt, attr in [
|
|
[" read", "uread"],
|
|
[" write", "uwrite"],
|
|
[" move", "umove"],
|
|
["delete", "udel"],
|
|
]:
|
|
u = list(sorted(getattr(v.axs, attr).keys()))
|
|
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
|
u = u if u else "\033[36m--none--\033[0m"
|
|
m += "\n| {}: {}".format(txt, u)
|
|
m += "\n"
|
|
|
|
if self.warn_anonwrite and not self.args.no_voldump:
|
|
self.log(m)
|
|
|
|
try:
|
|
v, _ = vfs.get("/", "*", False, True)
|
|
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
|
self.warn_anonwrite = False
|
|
msg = "anyone can read/write the current directory: {}"
|
|
self.log(msg.format(v.realpath), c=1)
|
|
except Pebkac:
|
|
self.warn_anonwrite = True
|
|
|
|
with self.mutex:
|
|
self.vfs = vfs
|
|
self.acct = acct
|
|
self.iacct = {v: k for k, v in acct.items()}
|
|
|
|
self.re_pwd = None
|
|
pwds = [re.escape(x) for x in self.iacct.keys()]
|
|
if pwds:
|
|
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
|
|
|
def dbg_ls(self):
|
|
users = self.args.ls
|
|
vols = "*"
|
|
flags = []
|
|
|
|
try:
|
|
users, vols = users.split(",", 1)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
vols, flags = vols.split(",", 1)
|
|
flags = flags.split(",")
|
|
except:
|
|
pass
|
|
|
|
if users == "**":
|
|
users = list(self.acct.keys()) + ["*"]
|
|
else:
|
|
users = [users]
|
|
|
|
for u in users:
|
|
if u not in self.acct and u != "*":
|
|
raise Exception("user not found: " + u)
|
|
|
|
if vols == "*":
|
|
vols = ["/" + x for x in self.vfs.all_vols.keys()]
|
|
else:
|
|
vols = [vols]
|
|
|
|
for v in vols:
|
|
if not v.startswith("/"):
|
|
raise Exception("volumes must start with /")
|
|
|
|
if v[1:] not in self.vfs.all_vols:
|
|
raise Exception("volume not found: " + v)
|
|
|
|
self.log({"users": users, "vols": vols, "flags": flags})
|
|
m = "/{}: read({}) write({}) move({}) del({})"
|
|
for k, v in self.vfs.all_vols.items():
|
|
vc = v.axs
|
|
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
|
|
|
|
flag_v = "v" in flags
|
|
flag_ln = "ln" in flags
|
|
flag_p = "p" in flags
|
|
flag_r = "r" in flags
|
|
|
|
n_bads = 0
|
|
for v in vols:
|
|
v = v[1:]
|
|
vtop = "/{}/".format(v) if v else "/"
|
|
for u in users:
|
|
self.log("checking /{} as {}".format(v, u))
|
|
try:
|
|
vn, _ = self.vfs.get(v, u, True, False, False, False)
|
|
except:
|
|
continue
|
|
|
|
atop = vn.realpath
|
|
g = vn.walk(
|
|
vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False
|
|
)
|
|
for _, _, vpath, apath, files, _, _ in g:
|
|
fnames = [n[0] for n in files]
|
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
|
vpaths = [vtop + x for x in vpaths]
|
|
apaths = [os.path.join(apath, n) for n in fnames]
|
|
files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths))
|
|
|
|
if flag_ln:
|
|
files = [x for x in files if not x[1].startswith(atop + os.sep)]
|
|
n_bads += len(files)
|
|
|
|
if flag_v:
|
|
msg = [
|
|
'# user "{}", vpath "{}"\n{}'.format(u, vp, ap)
|
|
for vp, ap in files
|
|
]
|
|
else:
|
|
msg = [x[1] for x in files]
|
|
|
|
if msg:
|
|
self.log("\n" + "\n".join(msg))
|
|
|
|
if n_bads and flag_p:
|
|
raise Exception("found symlink leaving volume, and strict is set")
|
|
|
|
if not flag_r:
|
|
sys.exit(0)
|