enforce single-instance for session/shares db

use file-locking to detect and prevent misconfigurations
which could lead to subtle unexpected behavior
This commit is contained in:
ed 2025-04-08 19:08:12 +00:00
parent 8e0364efad
commit acfaacbd46
3 changed files with 101 additions and 5 deletions

View file

@ -2135,7 +2135,9 @@ buggy feature? rip it out by setting any of the following environment variables
| env-var | what it does |
| -------------------- | ------------ |
| `PRTY_NO_DB_LOCK` | do not lock session/shares-databases for exclusive access |
| `PRTY_NO_IFADDR` | disable ip/nic discovery by poking into your OS with ctypes |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_IPV6` | disable some ipv6 support (should not be necessary since windows 2000) |
| `PRTY_NO_LZMA` | disable streaming xz compression of incoming uploads |
| `PRTY_NO_MP` | disable all use of the python `multiprocessing` module (actual multithreading, cpu-count for parsers/thumbnailers) |
@ -2676,7 +2678,6 @@ set any of the following environment variables to disable its associated optiona
| `PRTY_NO_CFSSL` | never attempt to generate self-signed certificates using [cfssl](https://github.com/cloudflare/cfssl) |
| `PRTY_NO_FFMPEG` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips |
| `PRTY_NO_FFPROBE` | **audio transcoding** goes byebye, **thumbnailing** must be handled by Pillow/libvips, **metadata-scanning** must be handled by mutagen |
| `PRTY_NO_IMPRESO` | do not try to load js/css files using `importlib.resources` |
| `PRTY_NO_MUTAGEN` | do not use [mutagen](https://pypi.org/project/mutagen/) for reading metadata from media files; will fallback to ffprobe |
| `PRTY_NO_PIL` | disable all [Pillow](https://pypi.org/project/pillow/)-based thumbnail support; will fallback to libvips or ffmpeg |
| `PRTY_NO_PILF` | disable Pillow `ImageFont` text rendering, used for folder thumbnails |

View file

@ -64,6 +64,7 @@ from .util import (
expat_ver,
gzip,
load_ipu,
lock_file,
min_ex,
mp,
odfusion,
@ -419,6 +420,7 @@ class SvcHub(object):
# the sessions-db is whatever, if something looks broken then just nuke it
db_path = self.args.ses_db
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
@ -444,7 +446,11 @@ class SvcHub(object):
raise
sver = 1
self._create_session_db(cur)
self._verify_session_db(cur, sver)
err = self._verify_session_db(cur, sver, db_path)
if err:
tries = 99
self.args.no_ses = True
self.log("root", err, 3)
break
except Exception as ex:
@ -460,6 +466,10 @@ class SvcHub(object):
db.close() # type: ignore
except:
pass
try:
os.unlink(db_lock)
except:
pass
os.unlink(db_path)
def _create_session_db(self, cur: "sqlite3.Cursor") -> None:
@ -476,7 +486,7 @@ class SvcHub(object):
cur.execute(cmd)
self.log("root", "created new sessions-db")
def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int) -> None:
def _verify_session_db(self, cur: "sqlite3.Cursor", sver: int, db_path: str) -> str:
# ensure writable (maybe owned by other user)
db = cur.connection
@ -488,6 +498,10 @@ class SvcHub(object):
except:
owner = 0
if not lock_file(db_path + ".lock"):
t = "the sessions-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --ses-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now disable sessions and instead use plaintext passwords in cookies."
return t % (db_path, owner)
vars = (("pid", os.getpid()), ("ts", int(time.time() * 1000)))
if owner:
# wear-estimate: 2 cells; offsets 0x10, 0x50, 0x19720
@ -505,6 +519,7 @@ class SvcHub(object):
db.commit()
cur.close()
db.close()
return ""
def setup_share_db(self) -> None:
al = self.args
@ -513,7 +528,7 @@ class SvcHub(object):
al.shr = ""
return
import sqlite3
assert sqlite3 # type: ignore # !rm
al.shr = al.shr.strip("/")
if "/" in al.shr or not al.shr:
@ -528,6 +543,7 @@ class SvcHub(object):
# the shares-db is important, so panic if something is wrong
db_path = self.args.shr_db
db_lock = db_path + ".lock"
try:
create = not os.path.getsize(db_path)
except:
@ -560,6 +576,12 @@ class SvcHub(object):
except:
owner = 0
if not lock_file(db_lock):
t = "the shares-db [%s] is already in use by another copyparty instance (pid:%d). This is not supported; please provide another database with --shr-db or give this copyparty-instance its entirely separate config-folder by setting another path in the XDG_CONFIG_HOME env-var. You can also disable this safeguard by setting env-var PRTY_NO_DB_LOCK=1. Will now panic."
t = t % (db_path, owner)
self.log("root", t, 1)
raise Exception(t)
sch1 = [
r"create table kv (k text, v int)",
r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",

View file

@ -114,8 +114,14 @@ IP6ALL = "0:0:0:0:0:0:0:0"
try:
import ctypes
import fcntl
HAVE_FCNTL = True
except:
HAVE_FCNTL = False
try:
import ctypes
import termios
except:
pass
@ -3940,6 +3946,73 @@ def hidedir(dp) -> None:
pass
_flocks = {}
def _lock_file_noop(ap: str) -> bool:
return True
def _lock_file_ioctl(ap: str) -> bool:
assert fcntl # type: ignore # !rm
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
# NOTE: the fcntl.lockf identifier is (pid,node);
# the lock will be dropped if os.close(os.open(ap))
# is performed anywhere else in this thread
try:
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
_flocks[ap] = fd
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
try:
os.close(fd)
except:
pass
if eno in (errno.EAGAIN, errno.EACCES):
return False
print("WARNING: unexpected errno %d from fcntl.lockf; %r" % (eno, ex))
return True
def _lock_file_windows(ap: str) -> bool:
try:
import msvcrt
try:
fd = _flocks.pop(ap)
os.close(fd)
except:
pass
fd = os.open(ap, os.O_RDWR | os.O_CREAT, 438)
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
return True
except Exception as ex:
eno = getattr(ex, "errno", -1)
if eno == errno.EACCES:
return False
print("WARNING: unexpected errno %d from msvcrt.locking; %r" % (eno, ex))
return True
if os.environ.get("PRTY_NO_DB_LOCK"):
lock_file = _lock_file_noop
elif ANYWIN:
lock_file = _lock_file_windows
elif HAVE_FCNTL:
lock_file = _lock_file_ioctl
else:
lock_file = _lock_file_noop
try:
if sys.version_info < (3, 10) or os.environ.get("PRTY_NO_IMPRESO"):
# py3.8 doesn't have .files