option to store state out-of-volume (mostly untested)

This commit is contained in:
ed 2021-06-10 01:27:04 +02:00
parent a359d64d44
commit d6bf300d80
14 changed files with 123 additions and 44 deletions

View file

@ -301,6 +301,18 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo
the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
## database location
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
* `--hist ~/.cache/copyparty -v ~/music::r:chist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
btw,
* markdown edits are always stored in a local `.hist` subdirectory
* on windows the volflag path is cyglike, so `/c/temp` means `C:\temp`
## metadata from audio files
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:

View file

@ -285,6 +285,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
ap2.add_argument("--hist", metavar="PATH", type=str, help="where to store volume state")
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")

View file

@ -5,6 +5,8 @@ import re
import os
import sys
import stat
import base64
import hashlib
import threading
from .__init__ import WINDOWS
@ -22,7 +24,14 @@ class VFS(object):
self.uadm = uadm # users who are regular admins
self.flags = flags # config switches
self.nodes = {} # child nodes
self.all_vols = {vpath: self} if realpath else {} # flattened recursive
self.histtab = None # all realpath->histpath
if realpath:
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.all_vols = {vpath: self} # flattened recursive
else:
self.histpath = None
self.all_vols = {}
def __repr__(self):
return "VFS({})".format(
@ -273,7 +282,8 @@ class AuthSrv(object):
self.reload()
def log(self, msg, c=0):
self.log_func("auth", msg, c)
if self.log_func:
self.log_func("auth", msg, c)
def laggy_iter(self, iterable):
"""returns [value,isFinalValue]"""
@ -467,6 +477,46 @@ class AuthSrv(object):
)
raise Exception("invalid config")
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:
if WINDOWS and vflag.startswith("/"):
vflag = "{}:\\{}".format(vflag[1], vflag[3:])
vol.histpath = vflag
elif self.args.hist:
for nch in range(len(hid)):
hpath = os.path.join(self.args.hist, hid[: nch + 1])
try:
os.makedirs(hpath)
except:
pass
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 = os.path.realpath(vol.histpath)
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
all_mte = {}
errors = False
for vol in vfs.all_vols.values():

View file

@ -1,5 +1,6 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
from copyparty.authsrv import AuthSrv
import sys
import time
@ -34,6 +35,9 @@ class MpWorker(object):
if not FAKE_MP:
signal.signal(signal.SIGINT, self.signal_handler)
# starting to look like a good idea
self.authsrv = AuthSrv(args, None, False)
# instantiate all services here (TODO: inheritance?)
self.httpsrv = HttpSrv(self, True)
self.httpsrv.disconnect_func = self.httpdrop

View file

@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
import threading
from .authsrv import AuthSrv
from .httpsrv import HttpSrv
from .broker_util import ExceptionalQueue, try_exec
@ -17,6 +18,9 @@ class BrokerThr(object):
self.mutex = threading.Lock()
# starting to look like a good idea
self.authsrv = AuthSrv(self.args, None, False)
# instantiate all services here (TODO: inheritance?)
self.httpsrv = HttpSrv(self)
self.httpsrv.disconnect_func = self.httpdrop

View file

@ -71,7 +71,7 @@ class HttpConn(object):
def get_u2idx(self):
if not self.u2idx:
self.u2idx = U2idx(self.args, self.log_func)
self.u2idx = U2idx(self.args, self.log_func, self.auth.vfs)
return self.u2idx

View file

@ -40,6 +40,7 @@ class HttpSrv(object):
self.is_mp = is_mp
self.args = broker.args
self.log = broker.log
self.auth = broker.authsrv
self.disconnect_func = None
self.mutex = threading.Lock()
@ -47,7 +48,6 @@ class HttpSrv(object):
self.clients = {}
self.workload = 0
self.workload_thr_alive = False
self.auth = AuthSrv(self.args, self.log)
env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))

View file

@ -44,7 +44,7 @@ class SvcHub(object):
# initiate all services to manage
self.tcpsrv = TcpSrv(self)
self.up2k = Up2k(self, auth.vfs.all_vols)
self.up2k = Up2k(self, auth.vfs)
self.thumbsrv = None
if not args.no_thumb:
@ -54,7 +54,7 @@ class SvcHub(object):
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
self.log("thumb", msg, c=3)
self.thumbsrv = ThumbSrv(self, auth.vfs.all_vols)
self.thumbsrv = ThumbSrv(self, auth.vfs)
else:
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n"
self.log(

View file

@ -2,7 +2,6 @@
from __future__ import print_function, unicode_literals
import os
import time
from .util import Cooldown
from .th_srv import thumb_path, THUMBABLE, FMT_FF
@ -12,6 +11,7 @@ class ThumbCli(object):
def __init__(self, broker):
self.broker = broker
self.args = broker.args
self.hist = broker.authsrv.vfs.histtab
# cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke)
@ -30,7 +30,8 @@ class ThumbCli(object):
if fmt == "w" and self.args.th_no_webp:
fmt = "j"
tpath = thumb_path(ptop, rem, mtime, fmt)
hist = self.hist[ptop]
tpath = thumb_path(hist, rem, mtime, fmt)
ret = None
try:
st = os.stat(tpath)

View file

@ -74,7 +74,7 @@ if HAVE_FFMPEG and HAVE_FFPROBE:
THUMBABLE.update(FMT_FF)
def thumb_path(ptop, rem, mtime, fmt):
def thumb_path(hist, rem, mtime, fmt):
# base16 = 16 = 256
# b64-lc = 38 = 1444
# base64 = 64 = 4096
@ -95,15 +95,16 @@ def thumb_path(ptop, rem, mtime, fmt):
h = hashlib.sha512(fsenc(fn)).digest()[:24]
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
return "{}/.hist/th/{}/{}.{:x}.{}".format(
ptop, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg"
return "{}/th/{}/{}.{:x}.{}".format(
hist, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg"
)
class ThumbSrv(object):
def __init__(self, hub, vols):
def __init__(self, hub, vfs):
self.hub = hub
self.vols = [v.realpath for v in vols.values()]
self.vols = [v.realpath for v in vfs.all_vols.values()]
self.hist = vfs.histtab
self.args = hub.args
self.log_func = hub.log
@ -153,7 +154,8 @@ class ThumbSrv(object):
return not self.nthr
def get(self, ptop, rem, mtime, fmt):
tpath = thumb_path(ptop, rem, mtime, fmt)
hist = self.hist[ptop]
tpath = thumb_path(hist, rem, mtime, fmt)
abspath = os.path.join(ptop, rem)
cond = threading.Condition()
with self.mutex:
@ -319,26 +321,29 @@ class ThumbSrv(object):
interval = self.args.th_clean
while True:
time.sleep(interval)
for vol in self.vols:
vol += "/.hist/th"
self.log("\033[Jcln {}/\033[A".format(vol))
self.clean(vol)
for vol, hist in self.hist.items():
if hist.startswith(vol):
self.log("\033[Jcln {}/\033[A".format(hist))
else:
self.log("\033[Jcln {} ({})/\033[A".format(hist, vol))
self.clean(hist)
self.log("\033[Jcln ok")
def clean(self, vol):
# self.log("cln {}".format(vol))
def clean(self, hist):
# self.log("cln {}".format(hist))
maxage = self.args.th_maxage
now = time.time()
prev_b64 = None
prev_fp = None
try:
ents = os.listdir(vol)
ents = os.listdir(hist)
except:
return
for f in sorted(ents):
fp = os.path.join(vol, f)
fp = os.path.join(hist, f)
cmp = fp.lower().replace("\\", "/")
# "top" or b64 prefix/full (a folder)

View file

@ -7,7 +7,7 @@ import time
import threading
from datetime import datetime
from .util import u8safe, s3dec, html_escape, Pebkac
from .util import s3dec, Pebkac
from .up2k import up2k_wark_from_hashlist
@ -19,9 +19,11 @@ except:
class U2idx(object):
def __init__(self, args, log_func):
def __init__(self, args, log_func, vfs):
self.args = args
self.log_func = log_func
self.vfs = vfs
self.timeout = args.srch_time
if not HAVE_SQLITE3:
@ -60,10 +62,11 @@ class U2idx(object):
if cur:
return cur
cur = _open(ptop)
if not cur:
db_path = os.path.join(self.vfs.histtab[ptop], "up2k.db")
if not os.path.exists(db_path):
return None
cur = sqlite3.connect(db_path).cursor()
self.cur[ptop] = cur
return cur
@ -262,9 +265,3 @@ class U2idx(object):
if identifier == self.active_id:
self.active_cur.connection.interrupt()
def _open(ptop):
db_path = os.path.join(ptop, ".hist", "up2k.db")
if os.path.exists(db_path):
return sqlite3.connect(db_path).cursor()

View file

@ -48,8 +48,10 @@ class Up2k(object):
* ~/.config flatfiles for active jobs
"""
def __init__(self, hub, all_vols):
def __init__(self, hub, vfs):
self.hub = hub
self.vfs = vfs
# TODO stop passing around vfs, do auth or broker instead
self.args = hub.args
self.log_func = hub.log
@ -94,10 +96,12 @@ class Up2k(object):
self.log("could not initialize sqlite3, will use in-memory registry only")
if self.args.no_fastboot:
self.deferred_init(all_vols)
self.deferred_init(vfs.all_vols)
else:
t = threading.Thread(
target=self.deferred_init, args=(all_vols,), name="up2k-deferred-init"
target=self.deferred_init,
args=(vfs.all_vols,),
name="up2k-deferred-init",
)
t.daemon = True
t.start()
@ -294,7 +298,7 @@ class Up2k(object):
return have_e2d
def register_vpath(self, ptop, flags):
db_path = os.path.join(ptop, ".hist", "up2k.db")
db_path = os.path.join(self.vfs.histtab[ptop], "up2k.db")
if ptop in self.registry:
try:
return [self.cur[ptop], db_path]
@ -314,7 +318,7 @@ class Up2k(object):
self.log(" ".join(sorted(a)) + "\033[0m")
reg = {}
path = os.path.join(ptop, ".hist", "up2k.snap")
path = os.path.join(self.vfs.histtab[ptop], "up2k.snap")
if "e2d" in flags and os.path.exists(path):
with gzip.GzipFile(path, "rb") as f:
j = f.read().decode("utf-8")
@ -338,7 +342,7 @@ class Up2k(object):
return None
try:
os.mkdir(os.path.join(ptop, ".hist"))
os.makedirs(self.vfs.histtab[ptop])
except:
pass
@ -379,7 +383,7 @@ class Up2k(object):
def _build_dir(self, dbw, top, excl, cdir):
self.pp.msg = "a{} {}".format(self.pp.n, cdir)
histdir = os.path.join(top, ".hist")
histdir = self.vfs.histtab[top]
ret = 0
g = statdir(self.log, not self.args.no_scandir, False, cdir)
for iname, inf in sorted(g):
@ -928,7 +932,7 @@ class Up2k(object):
def _create_v3(self, cur):
"""
collision in 2^(n/2) files where n = bits (6 bits/ch)
10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx
10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx 1<<(3*10)
12*6/2 = 2^36 = 68'719'476'736, 24.8mb idx
16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx
"""
@ -1366,7 +1370,7 @@ class Up2k(object):
except:
pass
path = os.path.join(k, ".hist", "up2k.snap")
path = os.path.join(self.vfs.histtab[k], "up2k.snap")
if not reg:
if k not in prev or prev[k] is not None:
prev[k] = None
@ -1380,7 +1384,7 @@ class Up2k(object):
return
try:
os.mkdir(os.path.join(k, ".hist"))
os.makedirs(self.vfs.histtab[k])
except:
pass

View file

@ -37,6 +37,7 @@ class Cfg(Namespace):
nih=True,
mtp=[],
mte="a",
hist=None,
**{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
)

View file

@ -18,8 +18,8 @@ from copyparty import util
class Cfg(Namespace):
def __init__(self, a=[], v=[], c=None):
ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()}
ex["mtp"] = []
ex["mte"] = "a"
ex2 = {"mtp": [], "mte": "a", "hist": None}
ex.update(ex2)
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)