291 lines
12 KiB
Python
291 lines
12 KiB
Python
'''
|
|
Copyright 2025-2026 AIIrondev
|
|
|
|
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
|
See Legal/LICENSE for the full license text.
|
|
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
|
For commercial licensing inquiries: https://github.com/AIIrondev
|
|
'''
|
|
"""
|
|
Centralized settings module to load configuration from config.json and provide
|
|
defaults for the web application and helper modules.
|
|
"""
|
|
import os
|
|
import json
|
|
import atexit
|
|
from threading import Lock
|
|
from pymongo import MongoClient as _PyMongoClient
|
|
|
|
# Base directory of this Web package
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Default values
|
|
DEFAULTS = {
|
|
'version': '2.6.5',
|
|
'debug': False,
|
|
'secret_key': 'Hsse783942h2342f342342i34hwebf8',
|
|
'host': '0.0.0.0',
|
|
'port': 443,
|
|
'mongodb': {
|
|
'host': 'localhost',
|
|
'port': 27017,
|
|
'db': 'Inventarsystem',
|
|
},
|
|
'scheduler': {
|
|
'interval_minutes': 1,
|
|
'backup_interval_hours': 24,
|
|
'enabled': True,
|
|
},
|
|
'ssl': {
|
|
'enabled': False,
|
|
'cert': 'ssl_certs/cert.pem',
|
|
'key': 'ssl_certs/key.pem',
|
|
},
|
|
'images': {
|
|
'thumbnail_size': [150, 150],
|
|
'preview_size': [400, 400],
|
|
},
|
|
'upload': {
|
|
'folder': os.path.join(BASE_DIR, 'uploads'),
|
|
'thumbnail_folder': os.path.join(BASE_DIR, 'thumbnails'),
|
|
'preview_folder': os.path.join(BASE_DIR, 'previews'),
|
|
'qrcode_folder': os.path.join(BASE_DIR, 'QRCodes'),
|
|
'max_size_mb': 10,
|
|
'image_max_size_mb': 15,
|
|
'video_max_size_mb': 100,
|
|
'allowed_extensions': ['png', 'jpg', 'jpeg', 'gif']
|
|
},
|
|
'paths': {
|
|
'backups': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'backups'),
|
|
'logs': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'logs'),
|
|
'deleted_archives': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'deleted_archives'),
|
|
},
|
|
'schoolPeriods': {
|
|
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"},
|
|
"2": {"start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)"},
|
|
"3": {"start": "09:45", "end": "10:30", "label": "3. Stunde (09:45 - 10:30)"},
|
|
"4": {"start": "10:30", "end": "11:15", "label": "4. Stunde (10:30 - 11:15)"},
|
|
"5": {"start": "11:30", "end": "12:15", "label": "5. Stunde (11:30 - 12:15)"},
|
|
"6": {"start": "12:15", "end": "13:00", "label": "6. Stunde (12:15 - 13:00)"},
|
|
"7": {"start": "13:30", "end": "14:15", "label": "7. Stunde (13:30 - 14:15)"},
|
|
"8": {"start": "14:15", "end": "15:00", "label": "8. Stunde (14:15 - 15:00)"},
|
|
"9": {"start": "15:15", "end": "16:00", "label": "9. Stunde (15:15 - 16:00)"},
|
|
"10": {"start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)"}
|
|
},
|
|
'modules': {
|
|
'library': {
|
|
'enabled': False
|
|
},
|
|
'student_cards': {
|
|
'enabled': False,
|
|
'default_borrow_days': 14,
|
|
'max_borrow_days': 365
|
|
}
|
|
}
|
|
}
|
|
|
|
# Load configuration file
|
|
CONFIG_PATH = os.path.join(BASE_DIR, '..', 'config.json')
|
|
try:
|
|
with open(CONFIG_PATH, 'r') as f:
|
|
_conf = json.load(f)
|
|
except Exception:
|
|
_conf = {}
|
|
|
|
# Helper to get nested values with defaults
|
|
def _get(conf, path, default):
|
|
cur = conf
|
|
for p in path:
|
|
if isinstance(cur, dict) and p in cur:
|
|
cur = cur[p]
|
|
else:
|
|
return default
|
|
return cur
|
|
|
|
|
|
def _get_bool_env(name, default):
|
|
value = os.getenv(name)
|
|
if value is None:
|
|
return default
|
|
return value.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
|
|
|
|
def _get_int_env(name, default):
|
|
value = os.getenv(name)
|
|
if value is None or not value.strip():
|
|
return int(default)
|
|
try:
|
|
return int(value)
|
|
except ValueError:
|
|
return int(default)
|
|
|
|
# Expose settings
|
|
APP_VERSION = _get(_conf, ['ver'], DEFAULTS['version'])
|
|
DEBUG = _get_bool_env('INVENTAR_DEBUG', _get(_conf, ['dbg'], DEFAULTS['debug']))
|
|
SECRET_KEY = str(os.getenv('INVENTAR_SECRET_KEY', _get(_conf, ['key'], DEFAULTS['secret_key'])))
|
|
HOST = _get(_conf, ['host'], DEFAULTS['host'])
|
|
PORT = _get(_conf, ['port'], DEFAULTS['port'])
|
|
|
|
# Database
|
|
MONGODB_HOST = _get(_conf, ['mongodb', 'host'], DEFAULTS['mongodb']['host'])
|
|
MONGODB_PORT = _get(_conf, ['mongodb', 'port'], DEFAULTS['mongodb']['port'])
|
|
MONGODB_DB = _get(_conf, ['mongodb', 'db'], DEFAULTS['mongodb']['db'])
|
|
|
|
# Optional environment overrides for containerized/runtime deployments.
|
|
MONGODB_HOST = os.getenv('INVENTAR_MONGODB_HOST', MONGODB_HOST)
|
|
MONGODB_PORT = int(os.getenv('INVENTAR_MONGODB_PORT', str(MONGODB_PORT)))
|
|
MONGODB_DB = os.getenv('INVENTAR_MONGODB_DB', MONGODB_DB)
|
|
MONGODB_MAX_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MAX_POOL_SIZE', 20)
|
|
MONGODB_MIN_POOL_SIZE = _get_int_env('INVENTAR_MONGODB_MIN_POOL_SIZE', 0)
|
|
MONGODB_MAX_IDLE_TIME_MS = _get_int_env('INVENTAR_MONGODB_MAX_IDLE_TIME_MS', 300000)
|
|
MONGODB_CONNECT_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_CONNECT_TIMEOUT_MS', 5000)
|
|
MONGODB_SERVER_SELECTION_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SERVER_SELECTION_TIMEOUT_MS', 5000)
|
|
MONGODB_SOCKET_TIMEOUT_MS = _get_int_env('INVENTAR_MONGODB_SOCKET_TIMEOUT_MS', 30000)
|
|
MONGODB_MAX_CONNECTING = _get_int_env('INVENTAR_MONGODB_MAX_CONNECTING', 2)
|
|
|
|
# Scheduler
|
|
SCHEDULER_INTERVAL_MIN = _get(_conf, ['scheduler', 'interval_minutes'], DEFAULTS['scheduler']['interval_minutes'])
|
|
BACKUP_INTERVAL_HOURS = _get(_conf, ['scheduler', 'backup_interval_hours'], DEFAULTS['scheduler']['backup_interval_hours'])
|
|
SCHEDULER_ENABLED = _get(_conf, ['scheduler', 'enabled'], DEFAULTS['scheduler']['enabled'])
|
|
|
|
# SSL
|
|
SSL_ENABLED = _get(_conf, ['ssl', 'enabled'], DEFAULTS['ssl']['enabled'])
|
|
SSL_CERT = _get(_conf, ['ssl', 'cert'], DEFAULTS['ssl']['cert'])
|
|
SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key'])
|
|
|
|
# School periods
|
|
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
|
|
|
# Optional feature modules
|
|
LIBRARY_MODULE_ENABLED = bool(_get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
|
STUDENT_CARDS_MODULE_ENABLED = bool(_get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
|
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
|
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
|
|
|
|
# Upload/paths
|
|
ALLOWED_EXTENSIONS = set(_get(_conf, ['allowed_extensions'], DEFAULTS['upload']['allowed_extensions']))
|
|
UPLOAD_FOLDER = _get(_conf, ['upload', 'folder'], DEFAULTS['upload']['folder'])
|
|
THUMBNAIL_FOLDER = _get(_conf, ['upload', 'thumbnail_folder'], DEFAULTS['upload']['thumbnail_folder'])
|
|
PREVIEW_FOLDER = _get(_conf, ['upload', 'preview_folder'], DEFAULTS['upload']['preview_folder'])
|
|
QR_CODE_FOLDER = _get(_conf, ['upload', 'qrcode_folder'], DEFAULTS['upload']['qrcode_folder'])
|
|
|
|
# Normalize to absolute paths to avoid cwd issues
|
|
if not os.path.isabs(UPLOAD_FOLDER):
|
|
UPLOAD_FOLDER = os.path.join(BASE_DIR, os.path.relpath(UPLOAD_FOLDER, BASE_DIR))
|
|
if not os.path.isabs(THUMBNAIL_FOLDER):
|
|
THUMBNAIL_FOLDER = os.path.join(BASE_DIR, os.path.relpath(THUMBNAIL_FOLDER, BASE_DIR))
|
|
if not os.path.isabs(PREVIEW_FOLDER):
|
|
PREVIEW_FOLDER = os.path.join(BASE_DIR, os.path.relpath(PREVIEW_FOLDER, BASE_DIR))
|
|
if not os.path.isabs(QR_CODE_FOLDER):
|
|
QR_CODE_FOLDER = os.path.join(BASE_DIR, os.path.relpath(QR_CODE_FOLDER, BASE_DIR))
|
|
MAX_UPLOAD_MB = _get(_conf, ['upload', 'max_size_mb'], DEFAULTS['upload']['max_size_mb'])
|
|
IMAGE_MAX_UPLOAD_MB = _get(_conf, ['upload', 'image_max_size_mb'], DEFAULTS['upload']['image_max_size_mb'])
|
|
VIDEO_MAX_UPLOAD_MB = _get(_conf, ['upload', 'video_max_size_mb'], DEFAULTS['upload']['video_max_size_mb'])
|
|
|
|
THUMBNAIL_SIZE_LIST = _get(_conf, ['images', 'thumbnail_size'], DEFAULTS['images']['thumbnail_size'])
|
|
PREVIEW_SIZE_LIST = _get(_conf, ['images', 'preview_size'], DEFAULTS['images']['preview_size'])
|
|
THUMBNAIL_SIZE = (int(THUMBNAIL_SIZE_LIST[0]), int(THUMBNAIL_SIZE_LIST[1])) if isinstance(THUMBNAIL_SIZE_LIST, (list, tuple)) else (150, 150)
|
|
PREVIEW_SIZE = (int(PREVIEW_SIZE_LIST[0]), int(PREVIEW_SIZE_LIST[1])) if isinstance(PREVIEW_SIZE_LIST, (list, tuple)) else (400, 400)
|
|
|
|
BACKUP_FOLDER = _get(_conf, ['paths', 'backups'], DEFAULTS['paths']['backups'])
|
|
LOGS_FOLDER = _get(_conf, ['paths', 'logs'], DEFAULTS['paths']['logs'])
|
|
DELETED_ARCHIVE_FOLDER = _get(_conf, ['paths', 'deleted_archives'], DEFAULTS['paths']['deleted_archives'])
|
|
|
|
# Optional environment overrides for writable storage mounts.
|
|
BACKUP_FOLDER = os.getenv('INVENTAR_BACKUP_FOLDER', BACKUP_FOLDER)
|
|
LOGS_FOLDER = os.getenv('INVENTAR_LOGS_FOLDER', LOGS_FOLDER)
|
|
DELETED_ARCHIVE_FOLDER = os.getenv('INVENTAR_DELETED_ARCHIVE_FOLDER', DELETED_ARCHIVE_FOLDER)
|
|
|
|
# Normalize backup and logs paths to absolute paths (similar to upload folders) to avoid
|
|
# permission issues caused by relative paths resolving to unintended working dirs.
|
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(BASE_DIR))
|
|
if not os.path.isabs(BACKUP_FOLDER):
|
|
BACKUP_FOLDER = os.path.join(PROJECT_ROOT, BACKUP_FOLDER)
|
|
if not os.path.isabs(LOGS_FOLDER):
|
|
LOGS_FOLDER = os.path.join(PROJECT_ROOT, LOGS_FOLDER)
|
|
if not os.path.isabs(DELETED_ARCHIVE_FOLDER):
|
|
DELETED_ARCHIVE_FOLDER = os.path.join(PROJECT_ROOT, DELETED_ARCHIVE_FOLDER)
|
|
|
|
# Optional key for field/file encryption at application level.
|
|
DATA_ENCRYPTION_KEY = os.getenv('INVENTAR_DATA_ENCRYPTION_KEY', '').strip()
|
|
|
|
|
|
_MONGO_CLIENT_CACHE = {}
|
|
_MONGO_CLIENT_LOCK = Lock()
|
|
|
|
|
|
class _MongoClientProxy:
|
|
def __init__(self, client):
|
|
self._client = client
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._client, name)
|
|
|
|
def __getitem__(self, name):
|
|
return self._client[name]
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def close(self):
|
|
return None
|
|
|
|
|
|
def _close_cached_mongo_clients():
|
|
with _MONGO_CLIENT_LOCK:
|
|
clients = list(_MONGO_CLIENT_CACHE.values())
|
|
_MONGO_CLIENT_CACHE.clear()
|
|
|
|
for proxy in clients:
|
|
try:
|
|
proxy._client.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
atexit.register(_close_cached_mongo_clients)
|
|
|
|
|
|
def MongoClient(*args, **kwargs):
|
|
"""Return a process-local MongoDB client configured from this settings module."""
|
|
explicit_host = 'host' in kwargs
|
|
explicit_port = 'port' in kwargs
|
|
host = args[0] if len(args) >= 1 else kwargs.pop('host', MONGODB_HOST)
|
|
port = args[1] if len(args) >= 2 else kwargs.pop('port', MONGODB_PORT)
|
|
client_kwargs = {
|
|
'maxPoolSize': MONGODB_MAX_POOL_SIZE,
|
|
'minPoolSize': MONGODB_MIN_POOL_SIZE,
|
|
'maxIdleTimeMS': MONGODB_MAX_IDLE_TIME_MS,
|
|
'connectTimeoutMS': MONGODB_CONNECT_TIMEOUT_MS,
|
|
'serverSelectionTimeoutMS': MONGODB_SERVER_SELECTION_TIMEOUT_MS,
|
|
'socketTimeoutMS': MONGODB_SOCKET_TIMEOUT_MS,
|
|
'maxConnecting': MONGODB_MAX_CONNECTING,
|
|
'retryWrites': True,
|
|
'retryReads': True,
|
|
}
|
|
client_kwargs.update(kwargs)
|
|
|
|
if len(args) >= 2 and not explicit_host and not explicit_port:
|
|
mongo_args = args
|
|
else:
|
|
mongo_args = (host, port)
|
|
|
|
cache_key = (
|
|
mongo_args,
|
|
tuple(sorted((key, repr(value)) for key, value in client_kwargs.items())),
|
|
)
|
|
|
|
with _MONGO_CLIENT_LOCK:
|
|
cached_client = _MONGO_CLIENT_CACHE.get(cache_key)
|
|
if cached_client is not None:
|
|
return cached_client
|
|
|
|
client = _PyMongoClient(*mongo_args, **client_kwargs)
|
|
|
|
cached_client = _MongoClientProxy(client)
|
|
_MONGO_CLIENT_CACHE[cache_key] = cached_client
|
|
return cached_client
|