add cache eviction

This commit is contained in:
ed 2021-05-25 19:46:35 +02:00
parent e55678e28f
commit 483dd527c6
8 changed files with 166 additions and 29 deletions

View File

@ -92,10 +92,10 @@ you may also want these, especially on servers:
* browser * browser
* ☑ tree-view * ☑ tree-view
* ☑ media player * ☑ media player
* thumbnails * thumbnails
* ☑ images * ☑ images using Pillow
* ☑ videos * ☑ videos using FFmpeg
* ✖ cache eviction * ☑ cache eviction (max-age; maybe max-size eventually)
* ☑ SPA (browse while uploading) * ☑ SPA (browse while uploading)
* if you use the file-tree on the left only, not folders in the file list * if you use the file-tree on the left only, not folders in the file list
* server indexing * server indexing
@ -106,7 +106,7 @@ you may also want these, especially on servers:
* ☑ viewer * ☑ viewer
* ☑ editor (sure why not) * ☑ editor (sure why not)
summary: it works! you can use it! (but technically not even close to beta) summary: it works!
# bugs # bugs

View File

@ -253,6 +253,9 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
ap2.add_argument("--thumbsz", metavar="WxH", default="352x352", help="thumbnail res") ap2.add_argument("--thumbsz", metavar="WxH", default="352x352", help="thumbnail res")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=1800, help="cleanup interval")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
ap2 = ap.add_argument_group('database options') ap2 = ap.add_argument_group('database options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database") ap2.add_argument("-e2d", action="store_true", help="enable up2k database")

View File

@ -286,7 +286,7 @@ class HttpCli(object):
# "embedded" resources # "embedded" resources
if self.vpath.startswith(".cpr"): if self.vpath.startswith(".cpr"):
if self.vpath.startswith(".cpr/ico/"): if self.vpath.startswith(".cpr/ico/"):
return self.tx_ico(self.vpath.split("/")[-1]) return self.tx_ico(self.vpath.split("/")[-1], exact=True)
static_path = os.path.join(E.mod, "web/", self.vpath[5:]) static_path = os.path.join(E.mod, "web/", self.vpath[5:])
return self.tx_file(static_path) return self.tx_file(static_path)
@ -1207,7 +1207,11 @@ class HttpCli(object):
self.log("{}, {}".format(logmsg, spd)) self.log("{}, {}".format(logmsg, spd))
return True return True
def tx_ico(self, ext): def tx_ico(self, ext, exact=False):
if not exact:
if ext.endswith("/"):
ext = "a.folder"
bad = re.compile(r"[](){}[]|^[0-9_-]*$") bad = re.compile(r"[](){}[]|^[0-9_-]*$")
n = ext.split(".")[1:][::-1] n = ext.split(".")[1:][::-1]
ext = "" ext = ""

View File

@ -11,6 +11,7 @@ import calendar
from .__init__ import PY2, WINDOWS, MACOS, VT100 from .__init__ import PY2, WINDOWS, MACOS, VT100
from .util import mp from .util import mp
from .authsrv import AuthSrv
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
from .up2k import Up2k from .up2k import Up2k
from .th_srv import ThumbSrv, HAVE_PIL from .th_srv import ThumbSrv, HAVE_PIL
@ -36,14 +37,17 @@ class SvcHub(object):
self.log = self._log_disabled if args.q else self._log_enabled self.log = self._log_disabled if args.q else self._log_enabled
# jank goes here
auth = AuthSrv(self.args, self.log, False)
# initiate all services to manage # initiate all services to manage
self.tcpsrv = TcpSrv(self) self.tcpsrv = TcpSrv(self)
self.up2k = Up2k(self) self.up2k = Up2k(self, auth.vfs.all_vols)
self.thumbsrv = None self.thumbsrv = None
if not args.no_thumb: if not args.no_thumb:
if HAVE_PIL: if HAVE_PIL:
self.thumbsrv = ThumbSrv(self) self.thumbsrv = ThumbSrv(self, auth.vfs.all_vols)
else: else:
msg = "need Pillow to create thumbnails; for example:\n {} -m pip install --user Pillow" msg = "need Pillow to create thumbnails; for example:\n {} -m pip install --user Pillow"
self.log("thumb", msg.format(os.path.basename(sys.executable)), c=3) self.log("thumb", msg.format(os.path.basename(sys.executable)), c=3)
@ -76,9 +80,13 @@ class SvcHub(object):
if self.thumbsrv: if self.thumbsrv:
self.thumbsrv.shutdown() self.thumbsrv.shutdown()
print("waiting for thumbsrv...") for n in range(200): # 10s
while not self.thumbsrv.stopped():
time.sleep(0.05) time.sleep(0.05)
if self.thumbsrv.stopped():
break
if n == 3:
print("waiting for thumbsrv...")
print("nailed it") print("nailed it")

View File

@ -1,5 +1,7 @@
import os import os
import time
from .util import Cooldown
from .th_srv import thumb_path, THUMBABLE, FMT_FF from .th_srv import thumb_path, THUMBABLE, FMT_FF
@ -8,6 +10,9 @@ class ThumbCli(object):
self.broker = broker self.broker = broker
self.args = broker.args self.args = broker.args
# cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke)
def get(self, ptop, rem, mtime): def get(self, ptop, rem, mtime):
ext = rem.rsplit(".")[-1].lower() ext = rem.rsplit(".")[-1].lower()
if ext not in THUMBABLE: if ext not in THUMBABLE:
@ -17,13 +22,22 @@ class ThumbCli(object):
return None return None
tpath = thumb_path(ptop, rem, mtime) tpath = thumb_path(ptop, rem, mtime)
ret = None
try: try:
st = os.stat(tpath) st = os.stat(tpath)
if st.st_size: if st.st_size:
return tpath ret = tpath
else:
return None return None
except: except:
pass pass
if ret:
tdir = os.path.dirname(tpath)
if self.cooldown.poke(tdir):
self.broker.put(False, "thumbsrv.poke", tdir)
return ret
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime) x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime)
return x.get() return x.get()

View File

@ -1,12 +1,14 @@
import os import os
import sys import sys
import time
import shutil
import base64 import base64
import hashlib import hashlib
import threading import threading
import subprocess as sp import subprocess as sp
from .__init__ import PY2 from .__init__ import PY2
from .util import fsenc, Queue from .util import fsenc, Queue, Cooldown
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, parse_ffprobe from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, parse_ffprobe
@ -74,13 +76,16 @@ def thumb_path(ptop, rem, mtime):
class ThumbSrv(object): class ThumbSrv(object):
def __init__(self, hub): def __init__(self, hub, vols):
self.hub = hub self.hub = hub
self.vols = [v.realpath for v in vols.values()]
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
res = hub.args.thumbsz.split("x") res = hub.args.thumbsz.split("x")
self.res = tuple([int(x) for x in res]) self.res = tuple([int(x) for x in res])
self.poke_cd = Cooldown(self.args.th_poke)
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.busy = {} self.busy = {}
@ -108,6 +113,10 @@ class ThumbSrv(object):
msg += ", ".join(missing) msg += ", ".join(missing)
self.log(msg, c=1) self.log(msg, c=1)
t = threading.Thread(target=self.cleaner)
t.daemon = True
t.start()
def log(self, msg, c=0): def log(self, msg, c=0):
self.log_func("thumb", msg, c) self.log_func("thumb", msg, c)
@ -233,3 +242,78 @@ class ThumbSrv(object):
] ]
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
r = p.communicate() r = p.communicate()
def poke(self, tdir):
if not self.poke_cd.poke(tdir):
return
ts = int(time.time())
try:
p1 = os.path.dirname(tdir)
p2 = os.path.dirname(p1)
for dp in [tdir, p1, p2]:
os.utime(fsenc(dp), (ts, ts))
except:
pass
def cleaner(self):
interval = self.args.th_clean
while True:
time.sleep(interval)
for vol in self.vols:
vol += "/.hist/th"
self.log("cln {}/".format(vol))
self.clean(vol)
def clean(self, vol):
# self.log("cln {}".format(vol))
maxage = self.args.th_maxage
now = time.time()
prev_b64 = None
prev_fp = None
try:
ents = os.listdir(vol)
except:
return
for f in sorted(ents):
fp = os.path.join(vol, f)
cmp = fp.lower().replace("\\", "/")
# "top" or b64 prefix/full (a folder)
if len(f) <= 3 or len(f) == 24:
age = now - os.path.getmtime(fp)
if age > maxage:
with self.mutex:
safe = True
for k in self.busy.keys():
if k.lower().replace("\\", "/").startswith(cmp):
safe = False
break
if safe:
self.log("rm -rf [{}]".format(fp))
shutil.rmtree(fp, ignore_errors=True)
else:
self.clean(fp)
continue
# thumb file
try:
b64, ts, ext = f.split(".")
if len(b64) != 24 or len(ts) != 8 or ext != "jpg":
raise Exception()
ts = int(ts, 16)
except:
if f != "dir.txt":
self.log("foreign file in thumbs dir: [{}]".format(fp), 1)
continue
if b64 == prev_b64:
self.log("rm replaced [{}]".format(fp))
os.unlink(prev_fp)
prev_b64 = b64
prev_fp = fp

View File

@ -32,7 +32,6 @@ from .util import (
s2hms, s2hms,
) )
from .mtag import MTag from .mtag import MTag
from .authsrv import AuthSrv
try: try:
HAVE_SQLITE3 = True HAVE_SQLITE3 = True
@ -49,10 +48,11 @@ class Up2k(object):
* ~/.config flatfiles for active jobs * ~/.config flatfiles for active jobs
""" """
def __init__(self, hub): def __init__(self, hub, all_vols):
self.hub = hub self.hub = hub
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
self.all_vols = all_vols
# config # config
self.salt = self.args.salt self.salt = self.args.salt
@ -92,9 +92,7 @@ class Up2k(object):
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
self.log("could not initialize sqlite3, will use in-memory registry only") self.log("could not initialize sqlite3, will use in-memory registry only")
# this is kinda jank have_e2d = self.init_indexes()
auth = AuthSrv(self.args, self.log_func, False)
have_e2d = self.init_indexes(auth)
if have_e2d: if have_e2d:
thr = threading.Thread(target=self._snapshot) thr = threading.Thread(target=self._snapshot)
@ -139,9 +137,9 @@ class Up2k(object):
return True, ret return True, ret
def init_indexes(self, auth): def init_indexes(self):
self.pp = ProgressPrinter() self.pp = ProgressPrinter()
vols = auth.vfs.all_vols.values() vols = self.all_vols.values()
t0 = time.time() t0 = time.time()
have_e2d = False have_e2d = False

View File

@ -124,6 +124,32 @@ class Counter(object):
self.v = absval self.v = absval
class Cooldown(object):
def __init__(self, maxage):
self.maxage = maxage
self.mutex = threading.Lock()
self.hist = {}
self.oldest = 0
def poke(self, key):
with self.mutex:
now = time.time()
ret = False
v = self.hist.get(key, 0)
if now - v > self.maxage:
self.hist[key] = now
ret = True
if self.oldest - now > self.maxage * 2:
self.hist = {
k: v for k, v in self.hist.items() if now - v < self.maxage
}
self.oldest = sorted(self.hist.values())[0]
return ret
class Unrecv(object): class Unrecv(object):
""" """
undo any number of socket recv ops undo any number of socket recv ops