diff --git a/Backup-DB.py b/Backup-DB.py deleted file mode 100755 index 3fd7258..0000000 --- a/Backup-DB.py +++ /dev/null @@ -1,134 +0,0 @@ -''' - 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 -''' -""" -Backup-DB.py - -Usage: - python Backup-DB.py \ - --uri mongodb://user:pass@host:port/ \ - --db my_database \ - --out /path/to/backup_folder -""" -import os -import csv -import sys -import argparse -from datetime import datetime -from pymongo import MongoClient - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Backup a MongoDB database to CSV files (one per collection)." - ) - parser.add_argument( - "--uri", "-u", - required=True, - help="MongoDB URI, e.g. mongodb://user:pass@host:port/" - ) - parser.add_argument( - "--db", "-d", - required=True, - help="Name of the database to back up" - ) - parser.add_argument( - "--out", "-o", - default=f"mongo_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - help="Output directory (will be created if it doesn't exist)" - ) - return parser.parse_args() - - -def flatten_dict(d, parent_key="", sep="."): - """ - Flatten nested dicts into a single level, concatenating keys with sep. - Lists are left as-is (converted to string later). - """ - items = {} - for k, v in d.items(): - new_key = f"{parent_key}{sep}{k}" if parent_key else k - if isinstance(v, dict): - items.update(flatten_dict(v, new_key, sep=sep)) - else: - items[new_key] = v - return items - - -def export_collection(db, coll_name, out_dir): - coll = db[coll_name] - cursor = coll.find() - docs = list(cursor) - if not docs: - print(f"[{coll_name}] No documents found, skipping.") - return - - # Flatten and collect all keys - all_keys = set() - flat_docs = [] - for doc in docs: - # Convert ObjectId and other non-serializable types - doc = {k: str(v) for k, v in doc.items()} - flat = flatten_dict(doc) - flat_docs.append(flat) - all_keys.update(flat.keys()) - - out_file = os.path.join(out_dir, f"{coll_name}.csv") - with open(out_file, "w", newline="", encoding="utf-8") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=sorted(all_keys)) - writer.writeheader() - for flat in flat_docs: - writer.writerow(flat) - - print(f"[{coll_name}] Exported {len(flat_docs)} docs → {out_file}") - - -def main(): - args = parse_args() - - # Create output directory - try: - os.makedirs(args.out, exist_ok=True) - print(f"Backing up database '{args.db}' → '{args.out}'") - except PermissionError: - print(f"Error: Permission denied when creating directory {args.out}") - print("Try running with sudo or to a different output directory") - return 1 - - # Verify write permissions - test_file = os.path.join(args.out, ".write_test") - try: - with open(test_file, 'w') as f: - f.write("test") - os.remove(test_file) - except (IOError, PermissionError) as e: - print(f"Error: Cannot write to output directory {args.out}") - print(f"Reason: {str(e)}") - print("Try running with sudo or to a different output directory") - return 1 - - try: - # Connect to MongoDB - client = MongoClient(args.uri) - # Test connection - client.server_info() - db = client[args.db] - - # Export each collection - for coll_name in db.list_collection_names(): - export_collection(db, coll_name, args.out) - - print("Backup complete.") - return 0 - except Exception as e: - print(f"Error: {str(e)}") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/Backup-Invoices.py b/Backup-Invoices.py deleted file mode 100644 index b440fa8..0000000 --- a/Backup-Invoices.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -""" -Backup-Invoices.py - -Exports all invoice records stored in ausleihungen.InvoiceData to dedicated -archive files (JSONL + CSV + metadata). - -Usage: - python Backup-Invoices.py \ - --uri mongodb://localhost:27017/ \ - --db Inventarsystem \ - --out /var/backups/invoice-archive/invoices-2026-04-06_12-00-00 -""" - -import argparse -import csv -import json -import os -import sys -from datetime import datetime - -from bson import ObjectId -from pymongo import MongoClient - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Export invoice records from ausleihungen to JSONL and CSV." - ) - parser.add_argument("--uri", "-u", required=True, help="MongoDB URI") - parser.add_argument("--db", "-d", required=True, help="MongoDB database name") - parser.add_argument( - "--out", - "-o", - required=True, - help=( - "Output file base path without extension, e.g. " - "/var/backups/invoice-archive/invoices-2026-04-06_12-00-00" - ), - ) - return parser.parse_args() - - -def to_jsonable(value): - if isinstance(value, ObjectId): - return str(value) - if isinstance(value, datetime): - return value.isoformat() - if isinstance(value, dict): - return {k: to_jsonable(v) for k, v in value.items()} - if isinstance(value, list): - return [to_jsonable(v) for v in value] - return value - - -def flatten_dict(data, parent_key="", sep="."): - items = {} - for key, value in data.items(): - new_key = f"{parent_key}{sep}{key}" if parent_key else str(key) - if isinstance(value, dict): - items.update(flatten_dict(value, new_key, sep=sep)) - elif isinstance(value, list): - items[new_key] = json.dumps(value, ensure_ascii=False) - else: - items[new_key] = value - return items - - -def normalize_invoice_record(doc): - invoice_data = doc.get("InvoiceData") or {} - record = { - "borrow_id": str(doc.get("_id", "")), - "borrow_status": doc.get("Status", ""), - "borrow_user": doc.get("User", ""), - "borrow_item": doc.get("Item", ""), - "borrow_start": doc.get("Start"), - "borrow_end": doc.get("End"), - "invoice_number": invoice_data.get("invoice_number", ""), - "invoice_amount": invoice_data.get("amount"), - "invoice_amount_text": invoice_data.get("amount_text", ""), - "invoice_damage_reason": invoice_data.get("damage_reason", ""), - "invoice_created_at": invoice_data.get("created_at"), - "invoice_created_by": invoice_data.get("created_by", ""), - "invoice_borrower": invoice_data.get("borrower", ""), - "invoice_item_id": invoice_data.get("item_id", ""), - "invoice_item_name": invoice_data.get("item_name", ""), - "invoice_item_code": invoice_data.get("item_code", ""), - "invoice_mark_destroyed": invoice_data.get("mark_destroyed", False), - "invoice_status_before": invoice_data.get("status_before_invoice", ""), - "invoice_raw": invoice_data, - } - return to_jsonable(record) - - -def write_jsonl(path, records): - with open(path, "w", encoding="utf-8") as handle: - for row in records: - handle.write(json.dumps(row, ensure_ascii=False) + "\n") - - -def write_csv(path, records): - if not records: - with open(path, "w", encoding="utf-8", newline="") as handle: - writer = csv.writer(handle) - writer.writerow(["info"]) - writer.writerow(["no invoice records found"]) - return - - flat_records = [flatten_dict(row) for row in records] - fieldnames = sorted({key for row in flat_records for key in row.keys()}) - - with open(path, "w", encoding="utf-8", newline="") as handle: - writer = csv.DictWriter(handle, fieldnames=fieldnames) - writer.writeheader() - for row in flat_records: - writer.writerow(row) - - -def write_metadata(path, db_name, out_base, count): - payload = { - "db": db_name, - "exported_at": datetime.utcnow().isoformat() + "Z", - "record_count": count, - "files": { - "jsonl": out_base + ".jsonl", - "csv": out_base + ".csv", - }, - "schema_note": "Source is ausleihungen.InvoiceData", - } - with open(path, "w", encoding="utf-8") as handle: - json.dump(payload, handle, ensure_ascii=False, indent=2) - - -def main(): - args = parse_args() - out_base = args.out - out_dir = os.path.dirname(out_base) or "." - - try: - os.makedirs(out_dir, exist_ok=True) - except Exception as exc: - print(f"Error: cannot create output directory {out_dir}: {exc}") - return 1 - - client = None - try: - client = MongoClient(args.uri) - client.server_info() - db = client[args.db] - ausleihungen = db["ausleihungen"] - - cursor = ausleihungen.find({"InvoiceData": {"$exists": True, "$ne": None}}) - records = [normalize_invoice_record(doc) for doc in cursor] - - jsonl_path = out_base + ".jsonl" - csv_path = out_base + ".csv" - meta_path = out_base + ".meta.json" - - write_jsonl(jsonl_path, records) - write_csv(csv_path, records) - write_metadata(meta_path, args.db, out_base, len(records)) - - print(f"Invoice backup complete: {len(records)} records") - print(f"JSONL: {jsonl_path}") - print(f"CSV: {csv_path}") - print(f"META: {meta_path}") - return 0 - except Exception as exc: - print(f"Error: {exc}") - return 1 - finally: - if client: - client.close() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/README.md b/README.md index 7365afe..636e7b7 100755 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ sudo ./install.sh --cleanup-old-remove-cron Das System läuft produktiv Docker-first. Deployments und Updates erfolgen über Release-Artefakte (Build-only), nicht über Quellcode-Pulls. -- Laufzeit-Stack: [docker-compose.yml](docker-compose.yml) +- Laufzeit-Stack: [docker-compose-multitenant.yml](docker-compose-multitenant.yml) - Build-Pipeline: [Dockerfile](Dockerfile) - Release-Pipeline: [.github/workflows/release-docker.yml](.github/workflows/release-docker.yml) - Frontend/Reverse Proxy: Nginx (Container) @@ -237,7 +237,7 @@ docker compose logs -f nginx ### Persistente Daten -Die Volumes sind in [docker-compose.yml](docker-compose.yml) definiert: +Die Volumes sind in [docker-compose-multitenant.yml](docker-compose-multitenant.yml) definiert: - MongoDB Daten - Uploads / Thumbnails / Previews / QRCodes diff --git a/Web/backup_invoices.py b/Web/backup_invoices.py new file mode 100644 index 0000000..95b5293 --- /dev/null +++ b/Web/backup_invoices.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +import argparse +import csv +import datetime +import json +import os +import sys + +try: + from bson import json_util +except ImportError: + json_util = None + +try: + from pymongo import MongoClient +except ImportError: + print('Error: pymongo is required to run invoice backups.', file=sys.stderr) + sys.exit(1) + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +CONFIG_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, '..', 'config.json')) + +DEFAULT_ARCHIVE_DIR = '/var/backups/invoice-archive' +DEFAULT_KEEP_DAYS = 3650 + + +def load_config(): + config = {} + try: + with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + config = json.load(f) + except Exception: + pass + return config + + +def resolve_mongo_settings(args): + config = load_config() + mongodb = config.get('mongodb', {}) if isinstance(config, dict) else {} + + host = os.getenv('INVENTAR_MONGODB_HOST') or args.mongo_host or mongodb.get('host') or 'localhost' + port = os.getenv('INVENTAR_MONGODB_PORT') or args.mongo_port or mongodb.get('port') or 27017 + db_name = os.getenv('INVENTAR_MONGODB_DB') or args.db_name or mongodb.get('db') or 'Inventarsystem' + uri = args.mongo_uri + + if isinstance(port, str) and port.strip().isdigit(): + port = int(port.strip()) + + return host, int(port), db_name, uri + + +def format_csv_value(value): + if value is None: + return '' + if isinstance(value, bool): + return 'true' if value else 'false' + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, datetime.datetime): + return value.isoformat() + if isinstance(value, (list, dict)): + return json.dumps(value, ensure_ascii=False) + return str(value) + + +def normalize_doc_for_json(doc): + if json_util is not None: + return json.loads(json_util.dumps(doc, default=json_util.default)) + return doc + + +def build_csv_row(document): + invoice_data = document.get('InvoiceData') or {} + corrections = document.get('InvoiceCorrections') or [] + return { + 'invoice_number': invoice_data.get('invoice_number', ''), + 'borrow_id': str(document.get('_id', '')), + 'status_before_invoice': invoice_data.get('status_before_invoice', '') or document.get('Status', ''), + 'borrower': document.get('User', '') or invoice_data.get('borrower', ''), + 'item': document.get('Item', ''), + 'amount': invoice_data.get('amount', ''), + 'currency': invoice_data.get('currency', 'EUR'), + 'created_at': format_csv_value(invoice_data.get('created_at')), + 'paid': invoice_data.get('paid', False), + 'paid_at': format_csv_value(invoice_data.get('paid_at')), + 'invoice_reason': invoice_data.get('damage_reason', ''), + 'corrections_count': len(corrections) if isinstance(corrections, list) else 0, + 'corrections': json.dumps(corrections, ensure_ascii=False) if corrections else '', + } + + +def write_jsonl(path, cursor): + with open(path, 'w', encoding='utf-8') as f: + for document in cursor: + if json_util is not None: + line = json_util.dumps(document, default=json_util.default) + else: + line = json.dumps(document, default=str, ensure_ascii=False) + f.write(line + '\n') + + +def write_csv(path, cursor): + fieldnames = [ + 'invoice_number', + 'borrow_id', + 'status_before_invoice', + 'borrower', + 'item', + 'amount', + 'currency', + 'created_at', + 'paid', + 'paid_at', + 'invoice_reason', + 'corrections_count', + 'corrections', + ] + with open(path, 'w', encoding='utf-8', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + for document in cursor: + writer.writerow({k: format_csv_value(v) for k, v in build_csv_row(document).items()}) + + +def write_meta(path, metadata): + with open(path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, ensure_ascii=False, indent=2) + + +def parse_args(): + parser = argparse.ArgumentParser(description='Create a legal invoice archive backup from the MongoDB invoice records.') + parser.add_argument('--archive-dir', required=True, help='Directory to write archive files into') + parser.add_argument('--base-name', default=None, help='Base filename prefix for archive files') + parser.add_argument('--mongo-host', default=None, help='MongoDB host override') + parser.add_argument('--mongo-port', type=int, default=None, help='MongoDB port override') + parser.add_argument('--db-name', default=None, help='MongoDB database name override') + parser.add_argument('--mongo-uri', default=None, help='MongoDB connection URI override') + return parser.parse_args() + + +def main(): + args = parse_args() + archive_dir = os.path.abspath(args.archive_dir) + os.makedirs(archive_dir, exist_ok=True) + + base_name = args.base_name or f'invoices-{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' + jsonl_path = os.path.join(archive_dir, f'{base_name}.jsonl') + csv_path = os.path.join(archive_dir, f'{base_name}.csv') + meta_path = os.path.join(archive_dir, f'{base_name}.meta.json') + + host, port, db_name, uri = resolve_mongo_settings(args) + if uri: + client = MongoClient(uri) + else: + client = MongoClient(host, port) + + try: + db = client[db_name] + collection = db['ausleihungen'] + query = {'InvoiceData.invoice_number': {'$exists': True, '$ne': ''}} + projection = {'InvoiceData': 1, 'InvoiceCorrections': 1, 'User': 1, 'Item': 1, 'Status': 1} + cursor = collection.find(query, projection) + + docs = list(cursor) + if not docs: + print('No invoice records found. No archive written.') + return 0 + + write_jsonl(jsonl_path, docs) + write_csv(csv_path, docs) + + metadata = { + 'generated_at': datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'invoice_count': len(docs), + 'archive_files': [os.path.basename(jsonl_path), os.path.basename(csv_path), os.path.basename(meta_path)], + 'mongo_db': db_name, + 'mongo_host': host, + 'mongo_port': port, + 'query': query, + } + write_meta(meta_path, metadata) + + print(f'Wrote invoice archive: {jsonl_path}') + print(f'Wrote invoice archive CSV: {csv_path}') + print(f'Wrote metadata: {meta_path}') + return 0 + finally: + client.close() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/archive-logs.sh b/archive-logs.sh deleted file mode 100755 index be7d6b9..0000000 --- a/archive-logs.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Monthly log archival script for Inventarsystem -# Runs on first day of month during automatic update -# Compresses logs to tar.gz, stores for up to 1 year, removes uncompressed originals - -PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -LOG_DIR="$PROJECT_DIR/logs" -ARCHIVE_BASE_DIR="$PROJECT_DIR/backups/logs_archive" -LOG_ARCHIVE_FILE="$LOG_DIR/archive-logs.log" -GUARD_FILE="$PROJECT_DIR/.last-log-archive" -COMPRESSION_LEVEL="${COMPRESSION_LEVEL:-9}" - -# Ensure archive base directory exists -mkdir -p "$ARCHIVE_BASE_DIR" -chmod 755 "$ARCHIVE_BASE_DIR" 2>/dev/null || true - -log_message() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_ARCHIVE_FILE" -} - -should_archive_logs() { - local today month_day guard_date - - today="$(date +%Y-%m-%d)" - month_day="$(date +%d)" - - # Only run on first day of month - if [ "$month_day" != "01" ]; then - return 1 - fi - - # Check if we already ran today - if [ -f "$GUARD_FILE" ]; then - guard_date="$(cat "$GUARD_FILE" 2>/dev/null || echo '')" - if [ "$guard_date" = "$today" ]; then - log_message "Log archive already executed today ($today). Skipping." - return 1 - fi - fi - - return 0 -} - -archive_application_logs() { - local archive_name archive_path retention_days cleanup_count deleted_count - - if [ ! -d "$LOG_DIR" ] || [ -z "$(find "$LOG_DIR" -maxdepth 1 -type f -name '*.log' 2>/dev/null)" ]; then - log_message "No log files found in $LOG_DIR. Nothing to archive." - return 0 - fi - - archive_name="inventar_logs_$(date +%Y-%m-01_%H%M%S)" - archive_path="$ARCHIVE_BASE_DIR/${archive_name}.tar.gz" - - log_message "Starting monthly log archival..." - log_message "Archive destination: $archive_path" - - # Create temporary directory for prepping files - local tmp_prep_dir - tmp_prep_dir="$(mktemp -d)" - trap 'rm -rf "${tmp_prep_dir:-}"' RETURN - - # Copy log files to temp directory (excluding archive log itself during copy) - if ! find "$LOG_DIR" -maxdepth 1 -type f -name '*.log' ! -name 'archive-logs.log' -exec cp {} "$tmp_prep_dir/" \; 2>/dev/null; then - log_message "WARNING: Could not copy all log files to temp directory" - fi - - # Create tar.gz archive - if tar -czf "$archive_path" -C "$tmp_prep_dir" . 2>/dev/null; then - log_message "Log archive created successfully: $archive_path" - log_message "Archive size: $(du -h "$archive_path" | cut -f1)" - else - log_message "ERROR: Failed to create log archive" - return 1 - fi - - # Remove uncompressed log files (keep archive-logs.log for continuity) - log_message "Removing uncompressed log files..." - local files_removed=0 - while IFS= read -r log_file; do - if [ "$log_file" != "$LOG_ARCHIVE_FILE" ]; then - rm -f "$log_file" && ((++files_removed)) || true - fi - done < <(find "$LOG_DIR" -maxdepth 1 -type f -name '*.log') - log_message "Removed $files_removed uncompressed log file(s)" - - trap - RETURN -} - -cleanup_old_archives() { - local retention_days cleanup_count deleted_count total_archives - - retention_days="365" - - # Find and remove archives older than 1 year - log_message "Cleaning up archives older than $retention_days days..." - - total_archives="$(find "$ARCHIVE_BASE_DIR" -maxdepth 1 -type f -name '*.tar.gz' 2>/dev/null | wc -l)" - deleted_count="0" - - find "$ARCHIVE_BASE_DIR" -maxdepth 1 -type f -name '*.tar.gz' -mtime +$retention_days | while read -r old_archive; do - log_message "Removing old archive: $old_archive" - rm -f "$old_archive" - ((++deleted_count)) || true - done 2>/dev/null || true - - # Count deleted (safer approach) - local current_archives - current_archives="$(find "$ARCHIVE_BASE_DIR" -maxdepth 1 -type f -name '*.tar.gz' 2>/dev/null | wc -l)" - deleted_count="$((total_archives - current_archives))" - - if [ "$deleted_count" -gt 0 ]; then - log_message "Deleted $deleted_count old archive(s); kept $current_archives" - else - log_message "No old archives to delete (total: $total_archives)" - fi -} - -main() { - if ! should_archive_logs; then - return 0 - fi - - log_message "=== Monthly log archival started ===" - - archive_application_logs || { - log_message "ERROR: Log archival failed" - return 1 - } - - cleanup_old_archives || { - log_message "WARNING: Archive cleanup encountered issues" - } - - # Update guard file with today's date - date +%Y-%m-%d > "$GUARD_FILE" - - log_message "=== Monthly log archival completed successfully ===" - return 0 -} - -main "$@" diff --git a/backup.sh b/backup.sh old mode 100755 new mode 100644 index a2e8a49..c1c9f04 --- a/backup.sh +++ b/backup.sh @@ -1,536 +1,161 @@ #!/usr/bin/env bash set -euo pipefail -# ======================================================== -# Inventarsystem backup helper -# - Creates a dated backup folder/archive under /var/backups (by default) -# - Exports MongoDB via run-backup.sh into mongodb_backup/ -# - Cleans up backups older than KEEP_DAYS -# -# Options: -# --dest|--out|--backup-dir Destination base directory (default: /var/backups) -# --db|--db-name MongoDB database name (default: Inventarsystem) -# --uri|--mongo-uri MongoDB URI (default: mongodb://localhost:27017/) -# --invoice-archive-dir Invoice archive directory (default: /invoice-archive) -# --invoice-keep-days Retention for invoice archive in days (default: 3650) -# --log Log file (default: ./logs/backup.log) -# --no-compress Disable compression (keeps directory instead of .tar.gz) -# --compress-level <0-9> Compression level flag (only 0 is special here) -# --keep-days Age-based retention in days (default: 7; 0 disables age filter) -# --min-keep Always keep at least N most recent backups (default: 7) -# --mode Backup mode (default: auto) -# -h|--help Show help -# ======================================================== - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="${PROJECT_DIR:-$SCRIPT_DIR}" -# Default destination now /var/backups; override with --dest or BACKUP_BASE_DIR env -BACKUP_BASE_DIR="${BACKUP_BASE_DIR:-/var/backups}" -LOG_DIR="$PROJECT_DIR/logs" -# Create only the log directory here; backup dir is created later with sudo if needed -mkdir -p "$LOG_DIR" -LOG_FILE="${LOG_FILE:-$LOG_DIR/backup.log}" -COMPRESSION_LEVEL="${COMPRESSION_LEVEL:-9}" -KEEP_DAYS="${KEEP_DAYS:-7}" -MIN_KEEP="${MIN_KEEP:-7}" -DB_NAME="${DB_NAME:-inventar_default}" -MONGO_URI="${MONGO_URI:-mongodb://localhost:27017/}" -INVOICE_KEEP_DAYS="${INVOICE_KEEP_DAYS:-3650}" -INVOICE_ARCHIVE_DIR="${INVOICE_ARCHIVE_DIR:-$BACKUP_BASE_DIR/invoice-archive}" -BACKUP_MODE="${BACKUP_MODE:-auto}" -COMPOSE_FILE="${COMPOSE_FILE:-docker-compose-multitenant.yml}" -DOCKER_AVAILABLE=0 -DOCKER_COMPOSE_CMD=() -USE_NULL_OUTPUT=false +COMPOSE_FILE="docker-compose-multitenant.yml" +INVOICE_ARCHIVE_DIR="${INVOICE_ARCHIVE_DIR:-/var/backups/invoice-archive}" +KEEP_DAYS="${INVOICE_KEEP_DAYS:-3650}" +MODE="auto" +BASE_NAME="" usage() { cat < Destination base directory (default: $BACKUP_BASE_DIR) - --db|--db-name MongoDB database name (default: $DB_NAME) - --uri|--mongo-uri MongoDB URI (default: $MONGO_URI) - --invoice-archive-dir Invoice archive directory (default: $INVOICE_ARCHIVE_DIR) - --invoice-keep-days Retention for invoice archive in days (default: $INVOICE_KEEP_DAYS) - --log Log file (default: $LOG_FILE) - --no-compress Disable compression (keeps directory instead of .tar.gz) - --compress-level <0-9> Compression level flag (only 0 is special here) - --keep-days Age-based retention in days (default: $KEEP_DAYS; 0 disables age filter) - --min-keep Always keep at least this many backups (default: $MIN_KEEP) - --mode Backup mode (default: $BACKUP_MODE) - --multitenant Use docker-compose-multitenant.yml (default) - --singletenant Use docker-compose.yml - -h|--help Show this help and exit + --invoice-archive-dir DIR Directory for invoice archive files + (default: $INVOICE_ARCHIVE_DIR) + --invoice-keep-days N Remove archived invoice files older than N days + (default: $KEEP_DAYS) + --mode auto|docker|host Backup mode (default: auto) + --base-name NAME Base filename prefix for archive files + -h, --help Show this help message EOF } -# Parse CLI arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --dest|--out|--backup-dir) - BACKUP_BASE_DIR="$2"; shift 2;; - --db|--db-name) - DB_NAME="$2"; shift 2;; - --uri|--mongo-uri) - MONGO_URI="$2"; shift 2;; - --invoice-archive-dir) - INVOICE_ARCHIVE_DIR="$2"; shift 2;; - --invoice-keep-days) - INVOICE_KEEP_DAYS="$2"; shift 2;; - --log) - LOG_FILE="$2"; shift 2;; - --no-compress) - COMPRESSION_LEVEL=0; shift;; - --compress-level) - COMPRESSION_LEVEL="${2:-6}"; shift 2;; - --keep-days) - KEEP_DAYS="$2"; shift 2;; - --min-keep) - MIN_KEEP="$2"; shift 2;; - --mode) - BACKUP_MODE="$2"; shift 2;; - --multitenant) - COMPOSE_FILE="docker-compose-multitenant.yml"; shift;; - --singletenant) - COMPOSE_FILE="docker-compose.yml"; shift;; - -h|--help) - usage; exit 0;; - *) - echo "Unknown option: $1" >&2; usage; exit 2;; - esac -done - -# Function to log messages -log_message() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" 2>/dev/null || echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --invoice-archive-dir) + INVOICE_ARCHIVE_DIR="$2" + shift 2 + ;; + --invoice-keep-days) + KEEP_DAYS="$2" + shift 2 + ;; + --mode) + MODE="$2" + shift 2 + ;; + --base-name) + BASE_NAME="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: unknown option '$1'" >&2 + usage + exit 2 + ;; + esac + done } -init_docker_compose() { - if docker compose version >/dev/null 2>&1; then - DOCKER_AVAILABLE=1 - DOCKER_COMPOSE_CMD=(docker compose -f "$PROJECT_DIR/$COMPOSE_FILE") - return 0 - fi - if sudo docker compose version >/dev/null 2>&1; then - DOCKER_AVAILABLE=1 - DOCKER_COMPOSE_CMD=(sudo docker compose -f "$PROJECT_DIR/$COMPOSE_FILE") - return 0 - fi - - DOCKER_AVAILABLE=0 - DOCKER_COMPOSE_CMD=() - return 1 +ensure_directory() { + mkdir -p "$INVOICE_ARCHIVE_DIR" } -compose_cmd() { - if [ "$DOCKER_AVAILABLE" -ne 1 ]; then +cleanup_old_archives() { + if [[ -n "$KEEP_DAYS" ]]; then + find "$INVOICE_ARCHIVE_DIR" -maxdepth 1 -type f \( -name 'invoices-*.jsonl' -o -name 'invoices-*.csv' -o -name 'invoices-*.meta.json' \) -mtime +"$KEEP_DAYS" -print -delete || true + fi +} + +host_backup() { + if ! command -v python3 >/dev/null 2>&1; then return 1 fi - "${DOCKER_COMPOSE_CMD[@]}" "$@" -} - -try_backup_db_docker() { - local out_dir="$1" - local stamp - stamp="$(date +"%Y-%m-%d_%H-%M-%S")" - local archive_path="$out_dir/mongodb-${stamp}.archive.gz" - - [ "$DOCKER_AVAILABLE" -eq 1 ] || return 1 - - log_message "Attempting Docker-based MongoDB backup..." - compose_cmd up -d mongodb >/dev/null 2>&1 || return 1 - if ! compose_cmd ps --status running mongodb | grep -q mongodb; then + if ! python3 -c 'import pymongo' >/dev/null 2>&1; then return 1 fi - if compose_cmd exec -T mongodb sh -c "mongodump --archive --gzip --db '$DB_NAME'" > "$archive_path"; then - log_message "Docker DB backup successful: $archive_path" - return 0 - fi - return 1 + python3 "$SCRIPT_DIR/Web/backup_invoices.py" \ + --archive-dir "$INVOICE_ARCHIVE_DIR" \ + --base-name "$BASE_NAME" } -try_backup_invoices_docker() { - local archive_base="$1" - [ "$DOCKER_AVAILABLE" -eq 1 ] || return 1 - - local tmp_base - tmp_base="/tmp/$(basename "$archive_base")" - - log_message "Attempting Docker-based invoice archive export..." - compose_cmd up -d mongodb app >/dev/null 2>&1 || return 1 - if ! compose_cmd ps --status running app | grep -q app; then +docker_backup() { + if ! command -v docker >/dev/null 2>&1; then + return 1 + fi + if ! docker compose -f "$COMPOSE_FILE" ps -q app >/dev/null 2>&1; then return 1 fi - local app_cid - app_cid="$(compose_cmd ps -q app)" - [ -n "$app_cid" ] || return 1 - - if ! sudo docker cp "$PROJECT_DIR/Backup-Invoices.py" "$app_cid:/tmp/Backup-Invoices.py" >/dev/null 2>&1; then + local app_container + app_container="$(docker compose -f "$COMPOSE_FILE" ps -q app | tr -d '[:space:]')" + if [[ -z "$app_container" ]]; then return 1 fi - if ! compose_cmd exec -T app python /tmp/Backup-Invoices.py \ - --uri "mongodb://mongodb:27017/" \ - --db "$DB_NAME" \ - --out "$tmp_base" >/dev/null 2>&1; then - return 1 - fi - - sudo docker cp "$app_cid:${tmp_base}.jsonl" "${archive_base}.jsonl" >/dev/null 2>&1 || return 1 - sudo docker cp "$app_cid:${tmp_base}.csv" "${archive_base}.csv" >/dev/null 2>&1 || return 1 - sudo docker cp "$app_cid:${tmp_base}.meta.json" "${archive_base}.meta.json" >/dev/null 2>&1 || return 1 - - compose_cmd exec -T app sh -c "rm -f /tmp/Backup-Invoices.py ${tmp_base}.jsonl ${tmp_base}.csv ${tmp_base}.meta.json" >/dev/null 2>&1 || true - return 0 -} - -backup_invoices_archive() { - log_message "Starting invoice archive export..." - - # Ensure archive dir exists and is writable - if [ ! -d "$INVOICE_ARCHIVE_DIR" ]; then - sudo mkdir -p "$INVOICE_ARCHIVE_DIR" - sudo chmod 755 "$INVOICE_ARCHIVE_DIR" - fi - local timestamp - timestamp="$(date +"%Y-%m-%d_%H-%M-%S")" - local archive_base - archive_base="$INVOICE_ARCHIVE_DIR/invoices-$timestamp" + timestamp="$(date +'%Y-%m-%d_%H-%M-%S')" + local output_dir + local remote_base - local py_exec - if [ -x "$PROJECT_DIR/.venv/bin/python" ]; then - py_exec="$PROJECT_DIR/.venv/bin/python" - else - py_exec="python3" - fi + output_dir="/tmp/invoice_archive_${timestamp}" + remote_base="${output_dir}/${BASE_NAME}" - local host_invoice_ok=0 - local prefer_docker_invoice=0 - if [ "$BACKUP_MODE" = "auto" ] && [ "$DOCKER_AVAILABLE" -eq 1 ] && [ "$MONGO_URI" = "mongodb://localhost:27017/" ]; then - prefer_docker_invoice=1 - log_message "Auto mode: preferring Docker invoice archive export" - fi + docker compose -f "$COMPOSE_FILE" exec -T app mkdir -p "$output_dir" + docker compose -f "$COMPOSE_FILE" exec -T app python3 /app/Web/backup_invoices.py \ + --archive-dir "$output_dir" \ + --base-name "$BASE_NAME" - if [ "$BACKUP_MODE" != "docker" ] && [ "$prefer_docker_invoice" -ne 1 ]; then - if [ "${USE_NULL_OUTPUT:-false}" = true ]; then - if sudo -E "$py_exec" "$PROJECT_DIR/Backup-Invoices.py" --uri "$MONGO_URI" --db "$DB_NAME" --out "$archive_base" > /dev/null 2>&1; then - host_invoice_ok=1 - fi - else - if sudo -E "$py_exec" "$PROJECT_DIR/Backup-Invoices.py" --uri "$MONGO_URI" --db "$DB_NAME" --out "$archive_base" >> "$PROJECT_DIR/logs/Backup_db.log" 2>&1; then - host_invoice_ok=1 - fi - fi - fi + mkdir -p "$INVOICE_ARCHIVE_DIR" + docker cp "$app_container":"${remote_base}.jsonl" "$INVOICE_ARCHIVE_DIR/" + docker cp "$app_container":"${remote_base}.csv" "$INVOICE_ARCHIVE_DIR/" + docker cp "$app_container":"${remote_base}.meta.json" "$INVOICE_ARCHIVE_DIR/" - if [ "$host_invoice_ok" -ne 1 ]; then - if [ "$BACKUP_MODE" = "host" ]; then - log_message "ERROR: Failed to export invoice archive in host mode" - return 1 - fi - if ! try_backup_invoices_docker "$archive_base"; then - log_message "ERROR: Failed to export invoice archive" - return 1 - fi - fi - - sudo chmod 640 "$archive_base".jsonl "$archive_base".csv "$archive_base".meta.json 2>/dev/null || true - log_message "Invoice archive written: $archive_base.(jsonl|csv|meta.json)" - - # Retention cleanup for invoice archive (default: 10 years) - if [ "$INVOICE_KEEP_DAYS" -gt 0 ]; then - local deleted_count=0 - while IFS= read -r old_file; do - [ -z "$old_file" ] && continue - if sudo rm -f "$old_file"; then - deleted_count=$((deleted_count + 1)) - fi - done < <(find "$INVOICE_ARCHIVE_DIR" -maxdepth 1 -type f \ - \( -name 'invoices-*.jsonl' -o -name 'invoices-*.csv' -o -name 'invoices-*.meta.json' \) \ - -mtime +"$INVOICE_KEEP_DAYS" -print 2>/dev/null) - - log_message "Invoice archive cleanup complete (retention: $INVOICE_KEEP_DAYS days, deleted files: $deleted_count)" - fi - - return 0 + docker compose -f "$COMPOSE_FILE" exec -T app rm -rf "$output_dir" } -# Function to create backup -create_backup() { - log_message "Starting backup process..." - - # Create date-formatted directory name - CURRENT_DATE=$(date +"%Y-%m-%d") - BACKUP_NAME="Inventarsystem-$CURRENT_DATE" - BACKUP_DIR="$BACKUP_BASE_DIR/$BACKUP_NAME" - BACKUP_ARCHIVE="$BACKUP_BASE_DIR/$BACKUP_NAME.tar.gz" - - # Create backup directory if it doesn't exist - if [ ! -d "$BACKUP_BASE_DIR" ]; then - sudo mkdir -p "$BACKUP_BASE_DIR" - sudo chmod 755 "$BACKUP_BASE_DIR" - fi - - # Remove existing backup with same date if it exists - if [ -d "$BACKUP_DIR" ]; then - log_message "Removing existing backup directory at $BACKUP_DIR" - sudo rm -rf "$BACKUP_DIR" - fi - - if [ -f "$BACKUP_ARCHIVE" ]; then - log_message "Removing existing backup archive at $BACKUP_ARCHIVE" - sudo rm -f "$BACKUP_ARCHIVE" - fi - - # Create a temporary directory for backup - log_message "Creating backup at $BACKUP_DIR" - sudo mkdir -p "$BACKUP_DIR" - # Copy project files excluding the backups directory itself (and optionally the venv) - if command -v rsync >/dev/null 2>&1; then - sudo rsync -a \ - --exclude='backups' \ - --exclude='.venv' \ - "$PROJECT_DIR"/ "$BACKUP_DIR"/ - else - # Fallback to cp while skipping the backups directory - for entry in "$PROJECT_DIR"/*; do - name=$(basename "$entry") - [[ "$name" == "backups" || "$name" == ".venv" ]] && continue - sudo cp -r "$entry" "$BACKUP_DIR/" - done - fi - - # Create database backup - log_message "Running database backup..." - # Create mongodb_backup directory with appropriate permissions first - sudo mkdir -p "$BACKUP_DIR/mongodb_backup" - sudo chmod 755 "$BACKUP_DIR/mongodb_backup" - - # Run database backup with our helper script - log_message "Executing database backup script..." - db_backup_ok=0 - use_host_db_backup=1 - if [ "$BACKUP_MODE" = "docker" ]; then - use_host_db_backup=0 - elif [ "$BACKUP_MODE" = "auto" ] && [ "$DOCKER_AVAILABLE" -eq 1 ] && [ "$MONGO_URI" = "mongodb://localhost:27017/" ]; then - use_host_db_backup=0 - log_message "Auto mode: preferring Docker DB backup" +main() { + if [[ -z "$BASE_NAME" ]]; then + BASE_NAME="invoices-$(date +'%Y-%m-%d_%H-%M-%S')" fi - if [ "$use_host_db_backup" -eq 1 ]; then - # Create Backup_db.log and ensure it's writable only when host mode is used - touch "$PROJECT_DIR/logs/Backup_db.log" 2>/dev/null - chmod 666 "$PROJECT_DIR/logs/Backup_db.log" 2>/dev/null || { - log_message "WARNING: Failed to set permissions on Backup_db.log. Trying with sudo..." - sudo touch "$PROJECT_DIR/logs/Backup_db.log" 2>/dev/null - sudo chmod 666 "$PROJECT_DIR/logs/Backup_db.log" 2>/dev/null || { - log_message "WARNING: Failed to create writable Backup_db.log. Redirecting output to /dev/null instead." - USE_NULL_OUTPUT=true - } - } + ensure_directory - # Install pymongo only for host backups - log_message "Checking pymongo installation..." - if ! python3 -c "import pymongo" &>/dev/null; then - log_message "Installing pymongo..." - if [ -f "$PROJECT_DIR/.venv/bin/pip" ]; then - if [ "$USE_NULL_OUTPUT" = true ]; then - "$PROJECT_DIR/.venv/bin/pip" install pymongo==4.6.3 > /dev/null 2>&1 || true - else - "$PROJECT_DIR/.venv/bin/pip" install pymongo==4.6.3 >> "$PROJECT_DIR/logs/Backup_db.log" 2>&1 || true - fi + case "$MODE" in + auto) + if docker_backup; then + cleanup_old_archives + return 0 fi - if ! python3 -c "import pymongo" &>/dev/null; then - if [ "$USE_NULL_OUTPUT" = true ]; then - pip3 install pymongo==4.6.3 > /dev/null 2>&1 || true - else - pip3 install pymongo==4.6.3 >> "$PROJECT_DIR/logs/Backup_db.log" 2>&1 || true - fi + if host_backup; then + cleanup_old_archives + return 0 fi - fi - - if [ "${USE_NULL_OUTPUT:-false}" = true ]; then - sudo -E "$PROJECT_DIR/run-backup.sh" --db "$DB_NAME" --uri "$MONGO_URI" --out "$BACKUP_DIR/mongodb_backup" > /dev/null 2>&1 && db_backup_ok=1 || { - log_message "ERROR: Failed to backup database with original path" - - # Try an alternative approach - use a temporary directory - log_message "Attempting backup via temporary directory..." - tmp_backup_dir="/tmp/mongodb_backup_$$" - mkdir -p "$tmp_backup_dir" - - "$PROJECT_DIR/run-backup.sh" --db "$DB_NAME" --uri "$MONGO_URI" --out "$tmp_backup_dir" > /dev/null 2>&1 && { - # Copy temporary backup files to the actual backup directory - log_message "Copying backup files from temporary directory..." - cp -r "$tmp_backup_dir"/* "$BACKUP_DIR/mongodb_backup/" - db_backup_ok=1 - } || { - log_message "ERROR: All attempts to backup database failed" - } - } - else - sudo -E "$PROJECT_DIR/run-backup.sh" --db "$DB_NAME" --uri "$MONGO_URI" --out "$BACKUP_DIR/mongodb_backup" >> "$PROJECT_DIR/logs/Backup_db.log" 2>&1 && db_backup_ok=1 || { - log_message "ERROR: Failed to backup database with original path" - - # Try an alternative approach - use a temporary directory - log_message "Attempting backup via temporary directory..." - tmp_backup_dir="/tmp/mongodb_backup_$$" - mkdir -p "$tmp_backup_dir" - - "$PROJECT_DIR/run-backup.sh" --db "$DB_NAME" --uri "$MONGO_URI" --out "$tmp_backup_dir" >> "$PROJECT_DIR/logs/Backup_db.log" 2>&1 && { - # Copy temporary backup files to the actual backup directory - log_message "Copying backup files from temporary directory..." - cp -r "$tmp_backup_dir"/* "$BACKUP_DIR/mongodb_backup/" - db_backup_ok=1 - } || { - log_message "ERROR: All attempts to backup database failed" - } - } - fi - else - log_message "Docker mode selected: skipping host DB backup step" - fi - - # Auto-fallback for docker-first environments - if [ "$db_backup_ok" -ne 1 ] && [ "$BACKUP_MODE" != "host" ]; then - if try_backup_db_docker "$BACKUP_DIR/mongodb_backup"; then - db_backup_ok=1 - fi - fi - - # Check if any CSV files were created during backup - if ! find "$BACKUP_DIR/mongodb_backup" -name "*.csv" -quit; then - log_message "WARNING: No CSV files found in backup directory" - - # If we used a temporary directory earlier and it still exists, try to use its contents - if [ -d "$tmp_backup_dir" ]; then - log_message "Backup created in temporary directory, moving to backup location..." - sudo cp -r "$tmp_backup_dir"/* "$BACKUP_DIR/mongodb_backup/" - rm -rf "$tmp_backup_dir" - fi - fi - - # Reset to more restrictive permissions after backup completes - sudo chmod -R 755 "$BACKUP_DIR/mongodb_backup" - - # Verify that backup files were created - csv_count=$(find "$BACKUP_DIR/mongodb_backup" -name "*.csv" 2>/dev/null | wc -l) - archive_count=$(find "$BACKUP_DIR/mongodb_backup" -name "*.archive.gz" 2>/dev/null | wc -l) - if [ "$csv_count" -gt 0 ] || [ "$archive_count" -gt 0 ]; then - log_message "Database backup successful: ${csv_count} CSV collection(s), ${archive_count} archive file(s)" - else - log_message "WARNING: No database backup files found in backup directory" - fi - - # Export legal invoice archive separately with long retention - backup_invoices_archive || log_message "WARNING: Invoice archive export failed" - - # Compress the backup - if [ "$COMPRESSION_LEVEL" -gt 0 ]; then - log_message "Compressing backup with level $COMPRESSION_LEVEL..." - - # Create compressed archive with pigz when available, otherwise gzip. - if command -v pigz >/dev/null 2>&1; then - gzip_cmd="pigz -${COMPRESSION_LEVEL}" - else - gzip_cmd="gzip -${COMPRESSION_LEVEL}" - fi - if tar --help 2>/dev/null | grep -q -- "--use-compress-program"; then - sudo tar -cf "$BACKUP_ARCHIVE" -I "$gzip_cmd" -C "$BACKUP_BASE_DIR" "$BACKUP_NAME" || { - log_message "ERROR: Failed to compress backup (tar -I)" - return 1 - } - else - GZIP="-${COMPRESSION_LEVEL}" sudo -E tar -czf "$BACKUP_ARCHIVE" -C "$BACKUP_BASE_DIR" "$BACKUP_NAME" || { - log_message "ERROR: Failed to compress backup (tar + GZIP env)" - return 1 - } - fi - - # Remove uncompressed directory after successful compression - if [ -f "$BACKUP_ARCHIVE" ]; then - log_message "Backup compressed successfully to $BACKUP_ARCHIVE" - log_message "Removing uncompressed backup directory" - sudo rm -rf "$BACKUP_DIR" - else - log_message "ERROR: Compressed backup file not found" + echo "Error: could not perform invoice backup in auto mode. Docker or host Python with pymongo is required." >&2 return 1 - fi - else - log_message "Compression disabled, keeping uncompressed backup" - fi - - # Set appropriate permissions - if [ "$COMPRESSION_LEVEL" -gt 0 ]; then - sudo chmod 644 "$BACKUP_ARCHIVE" - else - sudo chmod -R 755 "$BACKUP_DIR" - fi - - # Clean up old backups: age-based filter plus minimum keep safeguard - log_message "Cleaning up old backups (keep at least $MIN_KEEP; days>$KEEP_DAYS)..." - # Build list of all backup artifacts (files and directories) sorted newest first - mapfile -t ALL_BACKUPS < <(find "$BACKUP_BASE_DIR" -maxdepth 1 \ - \( -type f -name "Inventarsystem-*.tar.gz" -o -type d -name "Inventarsystem-*" \) \ - -printf '%T@\t%p\n' 2>/dev/null | sort -nr | awk -F '\t' '{print $2}') - TOTAL=${#ALL_BACKUPS[@]} - if (( TOTAL > MIN_KEEP )); then - # Determine deletion candidates by age (if KEEP_DAYS>0), otherwise all except the newest MIN_KEEP - if (( KEEP_DAYS > 0 )); then - mapfile -t AGE_CANDIDATES < <(find "$BACKUP_BASE_DIR" -maxdepth 1 \ - \( -type f -name "Inventarsystem-*.tar.gz" -o -type d -name "Inventarsystem-*" \) \ - -mtime +"$KEEP_DAYS" -printf '%T@\t%p\n' 2>/dev/null | sort -n | awk -F '\t' '{print $2}') - else - AGE_CANDIDATES=() - # If no age filter, consider everything except the newest MIN_KEEP as candidates (oldest first) - if (( TOTAL > MIN_KEEP )); then - # Reverse ALL_BACKUPS to get oldest first - for ((i=TOTAL-1; i>=MIN_KEEP; i--)); do AGE_CANDIDATES+=("${ALL_BACKUPS[$i]}"); done + ;; + docker) + if docker_backup; then + cleanup_old_archives + return 0 fi - fi - - # Protect the newest MIN_KEEP backups overall - ALLOWED_DELETE=$(( TOTAL - MIN_KEEP )) - DELETED=0 - for path in "${AGE_CANDIDATES[@]:-}"; do - (( DELETED >= ALLOWED_DELETE )) && break - if [ -z "$path" ]; then continue; fi - if [ -f "$path" ]; then - sudo rm -f "$path" && ((DELETED++)) - elif [ -d "$path" ]; then - sudo rm -rf "$path" && ((DELETED++)) + echo "Error: docker backup failed or app container is unavailable." >&2 + return 1 + ;; + host) + if host_backup; then + cleanup_old_archives + return 0 fi - done - log_message "Deleted $DELETED old backup(s); kept $(( TOTAL - DELETED ))" - else - log_message "No cleanup needed (total backups: $TOTAL <= min-keep: $MIN_KEEP)" - fi - - log_message "Backup completed successfully" - return 0 + echo "Error: host backup failed. Ensure python3 and pymongo are installed." >&2 + return 1 + ;; + *) + echo "Error: unsupported mode '$MODE'" >&2 + usage + return 2 + ;; + esac } -# Main -log_message "Backup destination: $BACKUP_BASE_DIR" -log_message "Database: $DB_NAME | URI: $MONGO_URI | Keep days: $KEEP_DAYS | Min keep: $MIN_KEEP | Compression: ${COMPRESSION_LEVEL}" -log_message "Invoice archive: $INVOICE_ARCHIVE_DIR | Invoice retention days: $INVOICE_KEEP_DAYS" -log_message "Backup mode: $BACKUP_MODE" - -if [[ "$BACKUP_MODE" != "auto" && "$BACKUP_MODE" != "host" && "$BACKUP_MODE" != "docker" ]]; then - log_message "ERROR: Invalid --mode value '$BACKUP_MODE' (allowed: auto, host, docker)" - exit 2 -fi - -init_docker_compose || true - -if create_backup; then - ART_DATE="$(date +"%Y-%m-%d")" - if [[ "$COMPRESSION_LEVEL" -gt 0 ]]; then - echo "$BACKUP_BASE_DIR/Inventarsystem-$ART_DATE.tar.gz" - else - echo "$BACKUP_BASE_DIR/Inventarsystem-$ART_DATE" - fi -fi \ No newline at end of file +parse_args "$@" +main diff --git a/cleanup-old.sh b/cleanup-old.sh deleted file mode 100755 index 780970e..0000000 --- a/cleanup-old.sh +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$SCRIPT_DIR" -ADMIN_PROJECT_DIR="${ADMIN_PROJECT_DIR:-$(dirname "$PROJECT_DIR")/admin_Inventarsystem}" - -DRY_RUN=0 -REMOVE_CRON=0 -KEEP_AUTOSTART=0 - -SERVICES=( - "inventarsystem-gunicorn.service" - "admin-inventarsystem-gunicorn.service" - "admin-inventarsystem-nginx.service" -) - -SOCKETS=( - "/tmp/inventarsystem.sock" - "/tmp/admin-inventarsystem.sock" -) - -PROCESS_PATTERNS=( - "$PROJECT_DIR/.venv/bin/gunicorn app:app" - "$ADMIN_PROJECT_DIR/.venv/bin/gunicorn app:app" -) - -CRON_FILTER_PATTERNS=( - "$PROJECT_DIR/update.sh" - "$PROJECT_DIR/backup-docker.sh" - "$ADMIN_PROJECT_DIR/update.sh" - "$ADMIN_PROJECT_DIR/backup-docker.sh" -) - -usage() { - cat </dev/null | awk '{print $1}' | grep -Fxq "$service" -} - -stop_services() { - if ! command -v systemctl >/dev/null 2>&1; then - log "systemctl not available, skipping service cleanup" - return 0 - fi - - local service - for service in "${SERVICES[@]}"; do - if ! service_exists "$service"; then - log "Service not found: $service" - continue - fi - - log "Stopping $service" - run_cmd_sudo systemctl stop "$service" || true - - if [ "$KEEP_AUTOSTART" -eq 0 ]; then - log "Disabling autostart for $service" - run_cmd_sudo systemctl disable "$service" || true - fi - done -} - -kill_leftover_processes() { - local pattern - for pattern in "${PROCESS_PATTERNS[@]}"; do - log "Terminating processes matching: $pattern" - run_cmd_sudo pkill -TERM -f "$pattern" || true - done - - if [ "$DRY_RUN" -eq 0 ]; then - sleep 2 - fi - - for pattern in "${PROCESS_PATTERNS[@]}"; do - run_cmd_sudo pkill -KILL -f "$pattern" || true - done -} - -remove_stale_sockets() { - local socket_path - for socket_path in "${SOCKETS[@]}"; do - if [ -e "$socket_path" ]; then - log "Removing stale socket/file: $socket_path" - run_cmd_sudo rm -f "$socket_path" - else - log "Socket/file not present: $socket_path" - fi - done -} - -remove_cron_entries() { - if ! command -v crontab >/dev/null 2>&1; then - log "crontab not available, skipping cron cleanup" - return 0 - fi - - local tmp_file - tmp_file="$(mktemp)" - - if [ "$(id -u)" -eq 0 ]; then - (crontab -l 2>/dev/null || true) > "$tmp_file" - else - (sudo crontab -l 2>/dev/null || true) > "$tmp_file" - fi - - local pattern - for pattern in "${CRON_FILTER_PATTERNS[@]}"; do - if [ "$DRY_RUN" -eq 1 ]; then - echo "[dry-run] would remove cron lines containing: $pattern" - else - grep -vF "$pattern" "$tmp_file" > "${tmp_file}.new" || true - mv "${tmp_file}.new" "$tmp_file" - fi - done - - if [ "$DRY_RUN" -eq 0 ]; then - if [ "$(id -u)" -eq 0 ]; then - crontab "$tmp_file" - else - sudo crontab "$tmp_file" - fi - fi - - rm -f "$tmp_file" - log "Cron cleanup finished" -} - -status_report() { - log "Remaining matching processes:" - ps -eo pid,ppid,cmd --sort=start_time | grep -E "Inventarsystem/.venv/bin/gunicorn|admin_Inventarsystem/.venv/bin/gunicorn" | grep -v grep || echo "none" - - log "Socket status:" - local socket_path - for socket_path in "${SOCKETS[@]}"; do - if [ -e "$socket_path" ]; then - echo "exists: $socket_path" - else - echo "missing: $socket_path" - fi - done -} - -main() { - parse_args "$@" - - stop_services - kill_leftover_processes - remove_stale_sockets - - if [ "$REMOVE_CRON" -eq 1 ]; then - remove_cron_entries - fi - - status_report - log "Cleanup complete" -} - -main "$@" diff --git a/debug_booking.py b/debug_booking.py deleted file mode 100644 index 3197950..0000000 --- a/debug_booking.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -""" -Debug script to check why planned bookings are not being activated -""" -import sys -import os -import datetime -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'Web')) - -from pymongo import MongoClient -import settings as cfg -import ausleihung as au - -def check_bookings(): - """Check all planned bookings and their status""" - client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) - db = client[cfg.MONGODB_DB] - ausleihungen = db['ausleihungen'] - - # Get all planned bookings - planned = list(ausleihungen.find({'Status': 'planned'})) - - print(f"\n📊 Gefundene GEPLANTE Ausleihen: {len(planned)}\n") - print("=" * 100) - - current_time = datetime.datetime.now() - print(f"⏰ Aktuelle Zeit: {current_time}\n") - - for booking in planned: - booking_id = str(booking['_id']) - user = booking.get('User', 'N/A') - item = booking.get('Item', 'N/A') - start = booking.get('Start') - end = booking.get('End') - period = booking.get('Period') - status = booking.get('Status') - - print(f"\n🔹 Ausleihe ID: {booking_id}") - print(f" Benutzer: {user}") - print(f" Item: {item}") - print(f" Status: {status}") - print(f" Schulstunde/Periode: {period}") - print(f" Start: {start} (Typ: {type(start).__name__})") - print(f" Ende: {end} (Typ: {type(end).__name__})") - - # Check current status - current_status = au.get_current_status(booking, log_changes=False) - print(f" ✓ Berechneter Status: {current_status}") - - if current_status != status: - print(f" ⚠️ STATUS STIMMT NICHT ÜBEREIN! Sollte sein: {current_status}") - - # Check time comparison - if start: - time_diff = (current_time - start).total_seconds() - if time_diff < 0: - print(f" ⏳ Startet in: {abs(time_diff) / 60:.1f} Minuten") - elif time_diff < 3600: - print(f" ✓ Sollte JETZT AKTIV sein! ({time_diff / 60:.1f} Minuten vorbei)") - else: - print(f" ✓ Sollte schon {time_diff / 3600:.1f} Stunden aktiv sein!") - - print("\n" + "=" * 100) - - # Check active bookings too - active = list(ausleihungen.find({'Status': 'active'})) - print(f"\n✓ AKTIVE Ausleihen: {len(active)}") - for booking in active: - print(f" - {booking.get('User', 'N/A')}: {booking.get('Item', 'N/A')} (Periode: {booking.get('Period')})") - - client.close() - -if __name__ == '__main__': - check_bookings() diff --git a/docker-compose-multitenant.yml b/docker-compose-multitenant.yml index 5f95b29..9d1235d 100644 --- a/docker-compose-multitenant.yml +++ b/docker-compose-multitenant.yml @@ -99,6 +99,7 @@ services: INVENTAR_REDIS_HOST: redis INVENTAR_REDIS_PORT: "6379" INVENTAR_REDIS_DB: "0" + INVENTAR_TENANT_PORT_MAP: "${INVENTAR_TENANT_PORT_MAP:-}" # Storage Configuration INVENTAR_BACKUP_FOLDER: /data/backups diff --git a/fix-all.sh b/fix-all.sh deleted file mode 100755 index 80a4f90..0000000 --- a/fix-all.sh +++ /dev/null @@ -1,1104 +0,0 @@ -#!/bin/bash -# -# fix-all.sh - All-in-One Reparaturskript für das Inventarsystem -# -# Dieses Skript führt alle notwendigen Reparaturschritte für das Inventarsystem durch: -# - Berechtigungen für alle Verzeichnisse und Dateien korrigieren -# - Virtual Environment und Python-Pakete reparieren -# - Git Repository-Berechtigungen setzen -# - Log-Verzeichnisse und Dateien erstellen und Berechtigungen setzen -# - Web-Verzeichnisberechtigungen korrigieren -# - MongoDB-Backup-Verzeichnisse einrichten -# - Bekannte Konflikte zwischen pymongo und bson beheben -# - Uploads-Verzeichnisse sowohl in Entwicklungs- als auch in Produktionsumgebungen verwalten -# -# Verwendung: sudo ./fix-all.sh [--check-only] [--verbose] [--fix-permissions] [--fix-venv] [--fix-pymongo] [--auto] [--setup-cron] - -# Parse command line arguments -CHECK_ONLY=false -VERBOSE=false -FIX_PERMISSIONS=true -FIX_VENV=true -FIX_PYMONGO=true -FIX_PNG_TO_JPG=true -AUTO_MODE=false -SETUP_CRON=false - -for arg in "$@"; do - case $arg in - --check-only) - CHECK_ONLY=true - shift - ;; - --verbose) - VERBOSE=true - shift - ;; - --fix-permissions) - FIX_PERMISSIONS=true - FIX_VENV=false - FIX_PYMONGO=false - shift - ;; - --fix-venv) - FIX_VENV=true - FIX_PERMISSIONS=false - FIX_PYMONGO=false - shift - ;; - --fix-pymongo) - FIX_PYMONGO=true - FIX_PERMISSIONS=false - FIX_VENV=false - FIX_PNG_TO_JPG=false - shift - ;; - --fix-png-jpg) - FIX_PNG_TO_JPG=true - FIX_PERMISSIONS=false - FIX_VENV=false - FIX_PYMONGO=false - shift - ;; - --no-fix-png-jpg) - FIX_PNG_TO_JPG=false - shift - ;; - --auto) - AUTO_MODE=true - shift - ;; - --setup-cron) - SETUP_CRON=true - shift - ;; - --email=*) - echo "Hinweis: Die E-Mail-Funktion wurde entfernt. Option \"$arg\" wird ignoriert." - shift - ;; - --help) - echo "Usage: $0 [OPTIONS]" - echo "Repariert alle bekannten Probleme im Inventarsystem." - echo "" - echo "Options:" - echo " --check-only Nur Prüfen, keine Änderungen durchführen" - echo " --verbose Ausführlichere Ausgabe" - echo " --fix-permissions Nur Berechtigungen korrigieren" - echo " --fix-venv Nur virtuelle Umgebung reparieren" - echo " --fix-pymongo Nur pymongo/bson-Konflikte beheben" - echo " --fix-png-jpg Nur PNG-zu-JPG Konvertierung durchführen" - echo " --no-fix-png-jpg PNG-zu-JPG Konvertierung überspringen" - echo " --auto Automatischer Modus - erkennt und behebt Probleme ohne Benutzerinteraktion" - echo " --setup-cron Richtet einen Cron-Job für regelmäßige Prüfungen ein" - echo " --help Diese Hilfe anzeigen" - echo "" - echo "Features:" - echo " - Intelligente Diagnose und zielgerichtete Reparatur" - echo " - Automatisches Erstellen und Verknüpfen von Upload-Verzeichnissen" - echo " - Erkennung und Korrektur von Entwicklungs- und Produktionsumgebungen" - echo " - Korrektur von Berechtigungsproblemen und fehlenden Verzeichnissen" - echo " - Behebt Probleme mit fehlenden Buch-Cover-Bildern in der Produktionsumgebung" - exit 0 - ;; - *) - # Unknown option - echo "Unbekannte Option: $arg" - echo "Verwenden Sie --help für Hilfe." - exit 1 - ;; - esac -done - -set -e # Exit on error - -# Get the script directory -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -VENV_DIR="$SCRIPT_DIR/.venv" -LOG_FILE="$SCRIPT_DIR/logs/fix_all.log" - -# Create logs directory immediately -mkdir -p "$SCRIPT_DIR/logs" -chmod -R 777 "$SCRIPT_DIR/logs" 2>/dev/null || true - -# Function to setup a cron job for automatic fixing -setup_cron_job() { - if ! command -v crontab &> /dev/null; then - echo "crontab nicht verfügbar. Cron-Job kann nicht eingerichtet werden." - return 1 - fi - - # Define cron job to run daily at 1:00 AM - CRON_JOB="0 1 * * * cd $SCRIPT_DIR && sudo $SCRIPT_DIR/fix-all.sh --auto --verbose >> $SCRIPT_DIR/logs/auto_fix.log 2>&1" - - # Check if cron job already exists - EXISTING_CRON=$(sudo crontab -l 2>/dev/null | grep -F "fix-all.sh --auto") - - if [ -z "$EXISTING_CRON" ]; then - echo "Richte täglichen Cron-Job für automatische Systemprüfung ein..." - - # Add to root's crontab (since we need sudo privileges) - (sudo crontab -l 2>/dev/null; echo "$CRON_JOB") | sudo crontab - - - if [ $? -eq 0 ]; then - echo "Cron-Job erfolgreich eingerichtet. Das System wird täglich um 1:00 Uhr morgens automatisch überprüft." - else - echo "Fehler beim Einrichten des Cron-Jobs." - return 1 - fi - else - echo "Cron-Job bereits vorhanden, keine Änderungen vorgenommen." - fi - - return 0 -} - -# Email-Versand entfernt: Berichte werden stattdessen in die Konsole und nach logs/auto_fix_report.log geschrieben - -# Function to handle automatic mode -handle_auto_mode() { - local has_problems=0 - local report="" - - # Check permissions - report+="Prüfung: Verzeichnisberechtigungen\n" - check_directory_permissions "$SCRIPT_DIR/logs" "777" "recursive" || has_problems=1 - check_directory_permissions "$SCRIPT_DIR/Web/uploads" "777" "recursive" 2>/dev/null || has_problems=1 - check_directory_permissions "$SCRIPT_DIR/Web/thumbnails" "777" "recursive" 2>/dev/null || has_problems=1 - check_directory_permissions "$SCRIPT_DIR/Web/previews" "777" "recursive" 2>/dev/null || has_problems=1 - check_directory_permissions "$SCRIPT_DIR/Web/QRCodes" "777" "recursive" 2>/dev/null || has_problems=1 - check_directory_permissions "$SCRIPT_DIR/mongodb_backup" "777" "recursive" 2>/dev/null || has_problems=1 - - if [ $has_problems -eq 1 ]; then - report+="Status: Probleme mit Berechtigungen gefunden, werden repariert\n" - else - report+="Status: Keine Berechtigungsprobleme gefunden\n" - fi - - # Check virtual environment - report+="\nPrüfung: Virtuelle Python-Umgebung\n" - set +e - check_venv_health - local venv_status=$? - set -e - - if [ $venv_status -ne 0 ]; then - has_problems=1 - report+="Status: Probleme mit virtueller Umgebung gefunden, werden repariert\n" - else - report+="Status: Virtuelle Umgebung ist gesund\n" - fi - - # Check for pymongo/bson conflict - report+="\nPrüfung: PyMongo/BSON-Konflikte\n" - set +e - check_pymongo_bson_conflict - local pymongo_status=$? - set -e - - if [ $pymongo_status -ne 0 ] && [ $pymongo_status -ne 2 ]; then - has_problems=1 - report+="Status: PyMongo/BSON-Konflikte gefunden, werden repariert\n" - elif [ $pymongo_status -eq 2 ]; then - report+="Status: PyMongo/BSON konnte nicht überprüft werden\n" - else - report+="Status: Keine PyMongo/BSON-Konflikte gefunden\n" - fi - - # Check system services - report+="\nPrüfung: Systemdienste\n" - set +e - check_services - local services_status=$? - set -e - - if [ $services_status -ne 0 ] && [ $services_status -ne 2 ]; then - has_problems=1 - report+="Status: Einige Dienste sind nicht aktiv, werden gestartet\n" - elif [ $services_status -eq 2 ]; then - report+="Status: Systemdienste konnten nicht überprüft werden\n" - else - report+="Status: Alle Dienste sind aktiv\n" - fi - - # If no problems or in check-only mode, just return - if [ $has_problems -eq 0 ]; then - report+="\nErgebnis: Alle Systeme funktionieren korrekt. Keine Reparaturen notwendig.\n" - # Print report to console and write to file - echo -e "$report" - echo -e "$report" >> "$SCRIPT_DIR/logs/auto_fix_report.log" - return 0 - fi - - if [ "$CHECK_ONLY" = true ]; then - report+="\nErgebnis: Probleme gefunden, aber nur Prüfung durchgeführt (--check-only).\n" - # Print report to console and write to file - echo -e "$report" - echo -e "$report" >> "$SCRIPT_DIR/logs/auto_fix_report.log" - return 1 - fi - - # Auto fix the issues - report+="\nStarte automatische Reparatur...\n" - - # Fix permissions if needed - if [ $has_problems -eq 1 ]; then - # All the fixes will be performed by the main script - report+="\nReparaturen wurden durchgeführt. Details im Log: $LOG_FILE\n" - fi - - # Print report to console and write to file - echo -e "$report" - echo -e "$report" >> "$SCRIPT_DIR/logs/auto_fix_report.log" - - return 0 -} - -# Function to log messages -log_message() { - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo -e "[\e[1;36m$timestamp\e[0m] $1" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "[\e[1;36m$timestamp\e[0m] $1" -} - -# Function to log success messages -log_success() { - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo -e "[\e[1;32m$timestamp\e[0m] ✓ $1" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "[\e[1;32m$timestamp\e[0m] ✓ $1" -} - -# Function to log error messages -log_error() { - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo -e "[\e[1;31m$timestamp\e[0m] ✗ $1" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "[\e[1;31m$timestamp\e[0m] ✗ $1" -} - -# Function to log warning messages -log_warning() { - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo -e "[\e[1;33m$timestamp\e[0m] ! $1" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "[\e[1;33m$timestamp\e[0m] ! $1" -} - -# Function to log verbose messages -log_verbose() { - if [ "$VERBOSE" = true ]; then - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - echo -e "[\e[1;90m$timestamp\e[0m] $1" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "[\e[1;90m$timestamp\e[0m] $1" - fi -} - -# Function to log section headers -log_section() { - echo "" - echo -e "\e[1;33m====== $1 ======\e[0m" | tee -a "$LOG_FILE" 2>/dev/null || echo -e "\e[1;33m====== $1 ======\e[0m" - echo "" -} - -# Function to check if a directory has correct permissions -check_directory_permissions() { - local dir="$1" - local expected_perm="$2" - local is_recursive="$3" - local result=0 - - if [ ! -d "$dir" ]; then - log_verbose "Verzeichnis nicht gefunden: $dir" - return 2 - fi - - if [ "$is_recursive" = "recursive" ]; then - find "$dir" -type d -not -perm "$expected_perm" | grep -q . && result=1 - else - [ "$(stat -c %a "$dir")" != "$expected_perm" ] && result=1 - fi - - if [ $result -eq 1 ]; then - return 1 - fi - return 0 -} - -# Function to check if a file has correct permissions -check_file_permissions() { - local file="$1" - local expected_perm="$2" - - if [ ! -f "$file" ]; then - log_verbose "Datei nicht gefunden: $file" - return 2 - fi - - if [ "$(stat -c %a "$file")" != "$expected_perm" ]; then - return 1 - fi - return 0 -} - -# Function to check if pymongo/bson conflict exists -check_pymongo_bson_conflict() { - if [ ! -d "$VENV_DIR" ]; then - log_verbose "Virtuelle Umgebung nicht gefunden" - return 2 - fi - - # Check for standalone bson package - if [ -d "$VENV_DIR/lib/python3.12/site-packages/bson" ] && [ -d "$VENV_DIR/lib/python3.12/site-packages/pymongo" ]; then - return 1 - fi - return 0 -} - -# Function to check if virtual environment is healthy -check_venv_health() { - if [ ! -d "$VENV_DIR" ]; then - log_verbose "Virtuelle Umgebung nicht gefunden" - return 2 - fi - - # Check for critical components - if [ ! -f "$VENV_DIR/bin/python" ] || [ ! -f "$VENV_DIR/bin/pip" ]; then - return 1 - fi - - # Try importing the runtime packages required at app startup - if ! "$VENV_DIR/bin/python" -c "import pymongo, flask, gunicorn, requests, PIL, apscheduler" 2>/dev/null; then - return 1 - fi - - return 0 -} - -# Function to create and fix upload directories in all environments -fix_uploads_directories() { - log_message "Erstelle und korrigiere Upload-Verzeichnisse in allen Umgebungen..." - - # Development environment directories - local DEV_DIRS=( - "$SCRIPT_DIR/Web/uploads" - "$SCRIPT_DIR/Web/thumbnails" - "$SCRIPT_DIR/Web/previews" - "$SCRIPT_DIR/Web/QRCodes" - ) - - # Production environment directories - local PROD_DIRS=( - "/var/Inventarsystem/Web/uploads" - "/var/Inventarsystem/Web/thumbnails" - "/var/Inventarsystem/Web/previews" - "/var/Inventarsystem/Web/QRCodes" - ) - - # Create and set permissions for development directories - for dir in "${DEV_DIRS[@]}"; do - if [ ! -d "$dir" ]; then - log_message "Erstelle Verzeichnis: $dir" - mkdir -p "$dir" 2>/dev/null || log_error "Fehler beim Erstellen von $dir" - fi - - log_message "Setze Berechtigungen für $dir" - chmod -R 777 "$dir" 2>/dev/null || log_error "Fehler beim Setzen von Berechtigungen für $dir" - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$dir" 2>/dev/null || log_error "Fehler beim Setzen von Eigentümerrechten für $dir" - done - - # Check if we are in production environment - if [ -d "/var/Inventarsystem" ] || [ -L "/var/Inventarsystem" ]; then - # Create and set permissions for production directories - for dir in "${PROD_DIRS[@]}"; do - if [ ! -d "$dir" ]; then - log_message "Erstelle Produktions-Verzeichnis: $dir" - mkdir -p "$dir" 2>/dev/null || log_error "Fehler beim Erstellen von $dir" - fi - - log_message "Setze Berechtigungen für $dir" - chmod -R 777 "$dir" 2>/dev/null || log_error "Fehler beim Setzen von Berechtigungen für $dir" - - # In production, use www-data as owner - chown -R www-data:www-data "$dir" 2>/dev/null || log_error "Fehler beim Setzen von Produktions-Eigentümerrechten für $dir" - done - - # Create symlinks if needed - for example if the app is looking in one path but files are in another - # This ensures that whether the app looks in dev or prod paths, it will find the files - if [ ! -L "/var/Inventarsystem/Web/uploads" ] && [ -d "$SCRIPT_DIR/Web/uploads" ]; then - # If production directory is empty but development has files, create symlink - if [ -z "$(ls -A "/var/Inventarsystem/Web/uploads" 2>/dev/null)" ] && [ -n "$(ls -A "$SCRIPT_DIR/Web/uploads" 2>/dev/null)" ]; then - log_message "Produktions-Upload-Verzeichnis ist leer, erstelle Symlink zu Entwicklungsverzeichnis" - rm -rf "/var/Inventarsystem/Web/uploads" 2>/dev/null - ln -sf "$SCRIPT_DIR/Web/uploads" "/var/Inventarsystem/Web/uploads" 2>/dev/null || log_error "Fehler beim Erstellen des Symlinks für uploads" - fi - fi - - # Ensure the application can find images in either location - log_message "Überprüfe Dateiübereinstimmung zwischen Entwicklungs- und Produktionsumgebung" - for env_dir in uploads thumbnails previews QRCodes; do - dev_dir="$SCRIPT_DIR/Web/$env_dir" - prod_dir="/var/Inventarsystem/Web/$env_dir" - - # If both directories exist and are not symlinks to each other - if [ -d "$dev_dir" ] && [ -d "$prod_dir" ] && [ ! -L "$prod_dir" ]; then - # Check for missing files in production that exist in development - for file in $(find "$dev_dir" -type f -name "*" 2>/dev/null); do - filename=$(basename "$file") - if [ ! -f "$prod_dir/$filename" ]; then - log_message "Kopiere fehlende Datei in die Produktionsumgebung: $filename" - cp -f "$file" "$prod_dir/" 2>/dev/null || log_error "Fehler beim Kopieren von $filename nach $prod_dir" - chmod 666 "$prod_dir/$filename" 2>/dev/null - fi - done - - # Check for missing files in development that exist in production - for file in $(find "$prod_dir" -type f -name "*" 2>/dev/null); do - filename=$(basename "$file") - if [ ! -f "$dev_dir/$filename" ]; then - log_message "Kopiere fehlende Datei in die Entwicklungsumgebung: $filename" - cp -f "$file" "$dev_dir/" 2>/dev/null || log_error "Fehler beim Kopieren von $filename nach $dev_dir" - chmod 666 "$dev_dir/$filename" 2>/dev/null - fi - done - fi - done - fi - - log_success "Upload-Verzeichnisse wurden erstellt und Berechtigungen gesetzt" -} - -# Function to ensure PNG images are converted to JPG for universal compatibility -fix_png_to_jpg_conversion() { - if [ "$FIX_PNG_TO_JPG" != true ]; then - log_verbose "PNG zu JPG Konvertierung übersprungen (--no-fix-png-jpg Flag gesetzt)" - return 0 - fi - - log_section "Überprüfe PNG zu JPG Konvertierung" - log_message "Das System verwendet einheitlich JPG-Dateien für maximale Kompatibilität." - - # Check if the converter script exists - if [ ! -f "$SCRIPT_DIR/png_jpg_converter.py" ]; then - log_error "PNG zu JPG Konverter-Skript nicht gefunden: png_jpg_converter.py" - return 1 - fi - - # Check if PIL/Pillow is installed in the virtual environment - if ! "$VENV_DIR/bin/pip" list | grep -i "pillow" >/dev/null 2>&1; then - log_message "Installiere Pillow für Bildkonvertierung..." - "$VENV_DIR/bin/pip" install pillow >/dev/null 2>&1 - if [ $? -ne 0 ]; then - log_error "Konnte Pillow nicht installieren" - return 1 - fi - log_success "Pillow erfolgreich installiert" - fi - - # Run the converter in dry-run mode first - log_message "Überprüfe PNG-Dateien im System..." - PNG_COUNT=$("$VENV_DIR/bin/python" "$SCRIPT_DIR/png_jpg_converter.py" | grep "Total PNG files:" | awk '{print $4}') - - if [ -z "$PNG_COUNT" ]; then - PNG_COUNT=0 - fi - - if [ "$PNG_COUNT" -eq 0 ]; then - log_success "Keine PNG-Dateien gefunden, System verwendet bereits einheitlich JPG-Dateien" - return 0 - fi - - log_message "Gefunden: $PNG_COUNT PNG-Dateien, die zu JPG konvertiert werden sollten" - - if [ "$CHECK_ONLY" = true ]; then - log_warning "Im Check-Only-Modus wird keine Konvertierung durchgeführt" - return 0 - fi - - # Confirm with user in interactive mode - if [ "$AUTO_MODE" != true ]; then - read -p "Möchten Sie alle PNG-Dateien zu JPG konvertieren? (j/n): " confirm - if [[ ! "$confirm" =~ ^[jJ] ]]; then - log_message "PNG zu JPG Konvertierung übersprungen" - return 0 - fi - fi - - # Run the converter with execute flag - log_message "Konvertiere PNG-Dateien zu JPG..." - "$VENV_DIR/bin/python" "$SCRIPT_DIR/png_jpg_converter.py" --execute --quality 95 - - if [ $? -eq 0 ]; then - log_success "PNG zu JPG Konvertierung erfolgreich abgeschlossen" - else - log_error "Fehler bei der PNG zu JPG Konvertierung" - return 1 - fi - - return 0 -} - -# Function to check if system services are running -check_services() { - if ! command -v systemctl >/dev/null 2>&1; then - log_verbose "systemctl nicht verfügbar" - return 2 - fi - - local services_status=0 - - if ! systemctl is-active --quiet mongodb 2>/dev/null; then - services_status=1 - fi - - if ! systemctl is-active --quiet inventarsystem-gunicorn.service 2>/dev/null; then - services_status=1 - fi - - if ! systemctl is-active --quiet inventarsystem-nginx.service 2>/dev/null; then - services_status=1 - fi - - return $services_status -} - -# Function to check if we are in production environment -check_production_environment() { - local prod_path="/var/Inventarsystem" - local in_prod=false - - if [ -d "$prod_path" ] || [ -L "$prod_path" ]; then - in_prod=true - log_message "Produktionsumgebung erkannt: $prod_path" - - # Verify directory structure - if [ ! -d "$prod_path/Web" ]; then - log_warning "Produktionsumgebung unvollständig: Web-Verzeichnis fehlt" - mkdir -p "$prod_path/Web" 2>/dev/null || log_error "Konnte Web-Verzeichnis nicht erstellen" - fi - else - log_verbose "Keine Produktionsumgebung unter $prod_path gefunden" - fi - - # Check if we're installed to /var/www - if [ -d "/var/www/Inventarsystem" ] || [ -L "/var/www/Inventarsystem" ]; then - in_prod=true - log_message "Alternative Produktionsumgebung erkannt: /var/www/Inventarsystem" - fi - - if [ "$in_prod" = true ]; then - return 0 - else - return 1 - fi -} - -# Check if running as root -if [ "$(id -u)" -ne 0 ]; then - log_error "Dieses Skript muss als root oder mit sudo ausgeführt werden" - log_error "Bitte starten Sie das Skript mit: sudo $0" - exit 1 -fi - -# Determine the actual user who invoked sudo -if [ -n "$SUDO_USER" ]; then - ACTUAL_USER="$SUDO_USER" - ACTUAL_GROUP="$(id -gn "$SUDO_USER")" -else - # If not using sudo, use the current user - ACTUAL_USER="$(whoami)" - ACTUAL_GROUP="$(id -gn)" -fi - -log_section "ALL-IN-ONE REPARATUR GESTARTET" -log_message "Benutzerinformation: $ACTUAL_USER:$ACTUAL_GROUP" -log_message "Systempfad: $SCRIPT_DIR" -if [ "$CHECK_ONLY" = true ]; then - log_message "Modus: Nur Prüfung (keine Änderungen werden durchgeführt)" -fi - -# Diagnostic phase - check what needs to be fixed -log_section "SYSTEMDIAGNOSE" -log_message "Überprüfe den aktuellen Systemzustand..." - -PERM_STATUS=0 -VENV_STATUS=0 -PYMONGO_STATUS=0 -SERVICES_STATUS=0 - -# Check permissions -log_message "Überprüfe Verzeichnisberechtigungen..." -check_directory_permissions "$SCRIPT_DIR/logs" "777" "recursive" || PERM_STATUS=1 -check_directory_permissions "$SCRIPT_DIR/Web/uploads" "777" "recursive" 2>/dev/null || PERM_STATUS=1 -check_directory_permissions "$SCRIPT_DIR/Web/thumbnails" "777" "recursive" 2>/dev/null || PERM_STATUS=1 -check_directory_permissions "$SCRIPT_DIR/Web/previews" "777" "recursive" 2>/dev/null || PERM_STATUS=1 -check_directory_permissions "$SCRIPT_DIR/Web/QRCodes" "777" "recursive" 2>/dev/null || PERM_STATUS=1 -check_directory_permissions "$SCRIPT_DIR/mongodb_backup" "777" "recursive" 2>/dev/null || PERM_STATUS=1 - -# Check virtual environment -log_message "Überprüfe virtuelle Python-Umgebung..." -set +e -check_venv_health -VENV_STATUS=$? -set -e - -# Check for pymongo/bson conflict -log_message "Überprüfe auf pymongo/bson-Konflikte..." -set +e -check_pymongo_bson_conflict -PYMONGO_STATUS=$? -set -e - -# Check system services -log_message "Überprüfe Systemdienste..." -set +e -check_services -SERVICES_STATUS=$? -set -e - -# Display diagnosis results -log_section "DIAGNOSEERGEBNISSE" -if [ $PERM_STATUS -eq 0 ]; then - log_success "Verzeichnisberechtigungen: Korrekt" -else - log_warning "Verzeichnisberechtigungen: Probleme gefunden" -fi - -if [ $VENV_STATUS -eq 0 ]; then - log_success "Virtuelle Python-Umgebung: Gesund" -elif [ $VENV_STATUS -eq 2 ]; then - log_warning "Virtuelle Python-Umgebung: Nicht gefunden" -else - log_warning "Virtuelle Python-Umgebung: Probleme gefunden" -fi - -if [ $PYMONGO_STATUS -eq 0 ]; then - log_success "PyMongo/BSON: Keine Konflikte gefunden" -elif [ $PYMONGO_STATUS -eq 2 ]; then - log_warning "PyMongo/BSON: Konnte nicht überprüft werden" -else - log_warning "PyMongo/BSON: Konflikt gefunden" -fi - -if [ $SERVICES_STATUS -eq 0 ]; then - log_success "Systemdienste: Alle aktiv" -elif [ $SERVICES_STATUS -eq 2 ]; then - log_warning "Systemdienste: Konnte nicht überprüft werden" -else - log_warning "Systemdienste: Einige Dienste sind nicht aktiv" -fi - -# Setup cron job if requested -if [ "$SETUP_CRON" = true ]; then - log_section "CRON-JOB EINRICHTUNG" - - if setup_cron_job; then - log_success "Cron-Job für automatische Prüfung wurde eingerichtet" - log_message "Das System wird täglich um 1:00 Uhr morgens automatisch überprüft und repariert" - else - log_error "Fehler beim Einrichten des Cron-Jobs" - fi - - # If only setting up cron job, exit here - if [ "$AUTO_MODE" = false ] && [ "$CHECK_ONLY" = true ]; then - exit 0 - fi -fi - -# Handle auto mode if enabled -if [ "$AUTO_MODE" = true ]; then - log_section "AUTOMATISCHER MODUS" - log_message "Automatischer Reparaturmodus ist aktiviert" - log_message "Überprüfe und behebe Probleme ohne Benutzerinteraktion..." - - # Let the auto mode handler determine if fixes are needed, but don't abort on non-zero due to set -e - set +e - handle_auto_mode - HM_STATUS=$? - set -e - - # If auto mode with check-only, exit here - if [ "$CHECK_ONLY" = true ]; then - log_section "AUTOMATISCHE PRÜFUNG ABGESCHLOSSEN" - log_message "Keine Änderungen wurden durchgeführt (--check-only ist aktiviert)" - log_message "Prüfbericht wurde in logs/auto_fix_report.log gespeichert" - exit $HM_STATUS - fi -fi - -# If check-only mode, exit here -if [ "$CHECK_ONLY" = true ]; then - log_section "PRÜFUNG ABGESCHLOSSEN" - log_message "Keine Änderungen wurden durchgeführt." - log_message "Um Probleme zu beheben, führen Sie das Skript ohne --check-only aus." - exit 0 -fi - -# Fix phase - only fix what's needed based on diagnosis -log_section "REPARATURPHASE" - -# Step 1: Create and fix permissions for critical directories -if [ $PERM_STATUS -ne 0 ] && [ "$FIX_PERMISSIONS" = true ]; then - log_section "VERZEICHNISBERECHTIGUNGEN KORRIGIEREN" - - # Logs directory - log_message "Berechtigungen für logs-Verzeichnis korrigieren..." - mkdir -p "$SCRIPT_DIR/logs" - chmod -R 777 "$SCRIPT_DIR/logs" 2>/dev/null || log_error "Fehler beim Setzen von Berechtigungen für logs-Verzeichnis" - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/logs" 2>/dev/null || log_error "Fehler beim Setzen von Eigentümerrechten für logs-Verzeichnis" - - # Create log files if they don't exist - log_message "Erstelle Log-Dateien mit korrekten Berechtigungen..." - touch "$SCRIPT_DIR/logs/Backup_db.log" "$SCRIPT_DIR/logs/daily_update.log" "$SCRIPT_DIR/logs/error.log" "$SCRIPT_DIR/logs/access.log" "$SCRIPT_DIR/logs/permission_fixes.log" "$SCRIPT_DIR/logs/fix_all.log" "$SCRIPT_DIR/logs/scheduler.log" "$SCRIPT_DIR/logs/restore.log" 2>/dev/null - chmod 666 "$SCRIPT_DIR/logs"/*.log 2>/dev/null - chown "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/logs"/*.log 2>/dev/null - - # Fix all upload directories in both development and production environments - fix_uploads_directories - - # Convert any PNG files to JPG for universal compatibility - fix_png_to_jpg_conversion - - # Fix Web directory permissions and structure - if [ -d "$SCRIPT_DIR/Web" ]; then - log_message "Berechtigungen für Web-Verzeichnis korrigieren..." - - # Set base permissions for Web directory - chmod -R 755 "$SCRIPT_DIR/Web" 2>/dev/null || log_error "Fehler beim Setzen von Berechtigungen für Web-Verzeichnis" - - # Set ownership - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/Web" 2>/dev/null || log_error "Fehler beim Setzen von Eigentümerrechten für Web-Verzeichnis" - - # Create and fix all uploads directories both in development and production - fix_uploads_directories - - log_success "Web-Verzeichnisberechtigungen korrigiert" - else - log_error "Web-Verzeichnis nicht gefunden: $SCRIPT_DIR/Web" - mkdir -p "$SCRIPT_DIR/Web" 2>/dev/null && { - log_message "Web-Verzeichnis wurde erstellt" - chmod 755 "$SCRIPT_DIR/Web" 2>/dev/null - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/Web" 2>/dev/null - fix_uploads_directories - } - fi - - # Create and set permissions for SSL certificates directory - if [ -d "$SCRIPT_DIR/certs" ]; then - log_message "Berechtigungen für SSL-Zertifikate korrigieren..." - chmod 755 "$SCRIPT_DIR/certs" 2>/dev/null - find "$SCRIPT_DIR/certs" -name "*.key" -exec chmod 600 {} \; 2>/dev/null - find "$SCRIPT_DIR/certs" -name "*.crt" -exec chmod 644 {} \; 2>/dev/null - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/certs" 2>/dev/null - log_success "SSL-Zertifikatberechtigungen korrigiert" - fi - - # Create and set permissions for backups directory - log_message "Berechtigungen für Backup-Verzeichnisse korrigieren..." - BACKUP_BASE_DIR="/var/backups" - if [ ! -d "$BACKUP_BASE_DIR" ]; then - mkdir -p "$BACKUP_BASE_DIR" 2>/dev/null - fi - chmod 755 "$BACKUP_BASE_DIR" 2>/dev/null || log_error "Fehler beim Setzen von Berechtigungen für $BACKUP_BASE_DIR" - - # Local backups directory - mkdir -p "$SCRIPT_DIR/backups" 2>/dev/null - chmod 777 "$SCRIPT_DIR/backups" 2>/dev/null - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/backups" 2>/dev/null - - # MongoDB backup directory - mkdir -p "$SCRIPT_DIR/mongodb_backup" 2>/dev/null - chmod 777 "$SCRIPT_DIR/mongodb_backup" 2>/dev/null - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR/mongodb_backup" 2>/dev/null - log_success "Backup-Verzeichnisberechtigungen korrigiert" - - # Step 2: Fix Git repository permissions - log_section "GIT REPOSITORY BERECHTIGUNGEN" - - log_message "Setze Git Repository als sicheres Verzeichnis..." - # For root - git config --global --add safe.directory "$SCRIPT_DIR" 2>/dev/null || log_error "Fehler beim Setzen des Git safe.directory für root" - - # For the actual user (if different from root) - if [ "$ACTUAL_USER" != "root" ]; then - su - "$ACTUAL_USER" -c "git config --global --add safe.directory '$SCRIPT_DIR'" 2>/dev/null || log_error "Fehler beim Setzen des Git safe.directory für $ACTUAL_USER" - fi - log_success "Git Repository-Berechtigungen korrigiert" - - # Step 3: Fix executable permissions for scripts - log_section "SKRIPT-BERECHTIGUNGEN" - - log_message "Mache alle Skripte ausführbar..." - find "$SCRIPT_DIR" -name "*.sh" -type f -exec chmod +x {} \; 2>/dev/null - find "$SCRIPT_DIR" -name "*.py" -type f -exec chmod +x {} \; 2>/dev/null - log_success "Skript-Berechtigungen korrigiert" -else - if [ "$FIX_PERMISSIONS" = true ]; then - log_success "Überspringe Berechtigungskorrekturen - alle Berechtigungen sind bereits korrekt" - else - log_verbose "Berechtigungskorrekturen wurden übersprungen (gemäß Befehlszeilenoption)" - fi -fi - -# Step 4: Fix virtual environment -if [ $VENV_STATUS -ne 0 ] && [ "$FIX_VENV" = true ]; then - log_section "PYTHON VIRTUELLE UMGEBUNG" - - log_message "Repariere virtuelle Python-Umgebung..." - if [ ! -d "$VENV_DIR" ]; then - log_message "Virtuelle Umgebung nicht gefunden. Erstelle neue Umgebung..." - python3 -m venv "$VENV_DIR" || { - log_error "Fehler beim Erstellen der virtuellen Umgebung" - log_message "Versuche alternative Methode..." - python3 -m venv "$VENV_DIR" --system-site-packages || { - log_error "Virtuelle Umgebung konnte nicht erstellt werden. Abbruch." - exit 1 - } - } - # Set ownership immediately after creation - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$VENV_DIR" 2>/dev/null - log_success "Neue virtuelle Umgebung erstellt" - else - log_message "Bestehende virtuelle Umgebung gefunden. Setze Berechtigungen..." - fi - - # Fix directory and file permissions - log_message "Setze Verzeichnis- und Dateirechte..." - find "$VENV_DIR" -type d -exec chmod 755 {} \; 2>/dev/null || log_error "Fehler beim Setzen der Verzeichnisberechtigungen" - find "$VENV_DIR" -type f -exec chmod 644 {} \; 2>/dev/null || log_error "Fehler beim Setzen der Dateiberechtigungen" - - # Make bin files executable - if [ -d "$VENV_DIR/bin" ]; then - log_message "Mache Bin-Dateien ausführbar..." - chmod 755 "$VENV_DIR/bin" 2>/dev/null - find "$VENV_DIR/bin" -type f -exec chmod 755 {} \; 2>/dev/null || log_error "Fehler beim Setzen der Bin-Dateirechte" - fi - - # Set ownership for virtual environment - log_message "Setze Eigentümerrechte für virtuelle Umgebung..." - chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$VENV_DIR" 2>/dev/null || log_error "Fehler beim Setzen der Eigentümerrechte für virtuelle Umgebung" - - log_success "Berechtigungen für virtuelle Umgebung korrigiert" -else - if [ "$FIX_VENV" = true ]; then - log_success "Überspringe Reparatur der virtuellen Umgebung - bereits in gutem Zustand" - else - log_verbose "Reparatur der virtuellen Umgebung wurde übersprungen (gemäß Befehlszeilenoption)" - fi -fi - -# Step 5: Fix pymongo/bson conflict -if [ $PYMONGO_STATUS -ne 0 ] && [ "$FIX_PYMONGO" = true ]; then - log_section "PYMONGO/BSON KONFLIKTE" - - # Detect bson/pymongo conflict - if [ -d "$VENV_DIR/lib/python3.12/site-packages/bson" ]; then - log_message "Potentieller bson/pymongo Konflikt erkannt. Behebe das Problem..." - rm -rf "$VENV_DIR/lib/python3.12/site-packages/bson" || { - log_error "Fehler beim Entfernen des Bson-Verzeichnisses. Versuche mit sudo..." - sudo rm -rf "$VENV_DIR/lib/python3.12/site-packages/bson" || { - log_error "Fehler beim Entfernen des Bson-Verzeichnisses." - } - } - log_message "Konfliktierendes Bson-Paket entfernt" - fi - - # Install/upgrade pip in the virtual environment - log_message "Installiere/aktualisiere pip in der virtuellen Umgebung..." - - # Try to activate the virtual environment and install packages - if [ -f "$VENV_DIR/bin/pip" ]; then - log_message "Benutze pip aus der virtuellen Umgebung..." - - # Remove conflicting packages - "$VENV_DIR/bin/pip" uninstall -y bson pymongo 2>/dev/null || true - - # Install pymongo - "$VENV_DIR/bin/pip" install --upgrade pip 2>/dev/null || log_error "Fehler beim Aktualisieren von pip" - "$VENV_DIR/bin/pip" install pymongo==4.6.1 2>/dev/null || { - log_error "Fehler beim Installieren von pymongo mit venv pip" - "$VENV_DIR/bin/python" -m pip install pymongo==4.6.1 2>/dev/null || { - log_error "Alle Versuche zur Installation von pymongo in der virtuellen Umgebung sind fehlgeschlagen" - } - } - - # Verify core runtime imports - if "$VENV_DIR/bin/python" -c "import pymongo, flask, gunicorn, requests, PIL, apscheduler" 2>/dev/null; then - log_success "Runtime-Abhängigkeiten wurden korrekt installiert" - else - log_error "Runtime-Abhängigkeiten konnten nicht verifiziert werden" - fi - - # Install requirements - if [ -f "$SCRIPT_DIR/requirements.txt" ]; then - log_message "Installiere Abhängigkeiten aus requirements.txt..." - # Filter out pymongo - grep -v "^pymongo" "$SCRIPT_DIR/requirements.txt" > "$SCRIPT_DIR/requirements_filtered.txt" - "$VENV_DIR/bin/pip" install -r "$SCRIPT_DIR/requirements_filtered.txt" 2>/dev/null || log_error "Fehler beim Installieren einiger Abhängigkeiten" - rm -f "$SCRIPT_DIR/requirements_filtered.txt" - fi - else - log_error "Pip nicht in der virtuellen Umgebung gefunden" - log_message "Versuche die Umgebung zu aktivieren und pip zu verwenden..." - - source "$VENV_DIR/bin/activate" 2>/dev/null && { - pip install --upgrade pip 2>/dev/null || log_error "Fehler beim Aktualisieren von pip" - pip uninstall -y bson pymongo 2>/dev/null || true - pip install pymongo==4.6.1 2>/dev/null || log_error "Fehler beim Installieren von pymongo" - - # Verify core runtime imports - if python -c "import pymongo, flask, gunicorn, requests, PIL, apscheduler" 2>/dev/null; then - log_success "Runtime-Abhängigkeiten wurden korrekt installiert" - else - log_error "Runtime-Abhängigkeiten konnten nicht verifiziert werden" - fi - - # Install requirements - if [ -f "$SCRIPT_DIR/requirements.txt" ]; then - log_message "Installiere Abhängigkeiten aus requirements.txt..." - grep -v "^pymongo" "$SCRIPT_DIR/requirements.txt" > "$SCRIPT_DIR/requirements_filtered.txt" - pip install -r "$SCRIPT_DIR/requirements_filtered.txt" 2>/dev/null || log_error "Fehler beim Installieren einiger Abhängigkeiten" - rm -f "$SCRIPT_DIR/requirements_filtered.txt" - fi - - deactivate - } || log_error "Konnte virtuelle Umgebung nicht aktivieren" - fi -else - if [ "$FIX_PYMONGO" = true ]; then - log_success "Überspringe PyMongo/BSON-Reparatur - keine Konflikte gefunden" - else - log_verbose "PyMongo/BSON-Reparatur wurde übersprungen (gemäß Befehlszeilenoption)" - fi -fi - -# Step 6: Check system service status -log_section "SYSTEMDIENSTE ÜBERPRÜFEN" - -log_message "Überprüfe Systemdienste..." -if command -v systemctl >/dev/null 2>&1; then - if systemctl is-active --quiet mongodb; then - log_success "MongoDB-Dienst läuft" - else - log_warning "MongoDB-Dienst läuft nicht" - log_message "Versuche MongoDB zu starten..." - systemctl start mongodb 2>/dev/null || log_error "Konnte MongoDB-Dienst nicht starten" - fi - - # Check for Inventarsystem services - if systemctl is-active --quiet inventarsystem-gunicorn.service 2>/dev/null; then - log_success "Inventarsystem Gunicorn-Dienst läuft" - else - log_warning "Inventarsystem Gunicorn-Dienst läuft nicht" - fi - - if systemctl is-active --quiet inventarsystem-nginx.service 2>/dev/null; then - log_success "Inventarsystem Nginx-Dienst läuft" - else - log_warning "Inventarsystem Nginx-Dienst läuft nicht" - fi -else - log_message "Systemctl nicht verfügbar. Dienststatus kann nicht überprüft werden." -fi - -# Step 7: Set correct ownership for entire project -log_section "PROJEKT-EIGENTÜMERRECHTE" - -log_message "Setze Eigentümerrechte für das gesamte Projekt..." -chown -R "$ACTUAL_USER:$ACTUAL_GROUP" "$SCRIPT_DIR" 2>/dev/null || log_error "Fehler beim Setzen der Eigentümerrechte für das gesamte Projekt" - -log_success "Projekt-Eigentümerrechte gesetzt" - -# Run a second diagnostic to confirm fixes -log_section "REPARATURVERIFIKATION" -log_message "Überprüfe den Systemzustand nach Reparaturen..." - -PERM_STATUS_AFTER=0 -VENV_STATUS_AFTER=0 -PYMONGO_STATUS_AFTER=0 -SERVICES_STATUS_AFTER=0 - -# Check permissions -check_directory_permissions "$SCRIPT_DIR/logs" "777" "recursive" || PERM_STATUS_AFTER=1 -check_directory_permissions "$SCRIPT_DIR/Web/uploads" "777" "recursive" 2>/dev/null || PERM_STATUS_AFTER=1 -check_directory_permissions "$SCRIPT_DIR/Web/thumbnails" "777" "recursive" 2>/dev/null || PERM_STATUS_AFTER=1 -check_directory_permissions "$SCRIPT_DIR/Web/previews" "777" "recursive" 2>/dev/null || PERM_STATUS_AFTER=1 -check_directory_permissions "$SCRIPT_DIR/Web/QRCodes" "777" "recursive" 2>/dev/null || PERM_STATUS_AFTER=1 -check_directory_permissions "$SCRIPT_DIR/mongodb_backup" "777" "recursive" 2>/dev/null || PERM_STATUS_AFTER=1 - -# Check virtual environment -set +e -check_venv_health -VENV_STATUS_AFTER=$? -set -e - -# Check for pymongo/bson conflict -set +e -check_pymongo_bson_conflict -PYMONGO_STATUS_AFTER=$? -set -e - -# Check system services -set +e -check_services -SERVICES_STATUS_AFTER=$? -set -e - -# Report fixed and remaining issues -if [ $PERM_STATUS_AFTER -eq 0 ] && [ $PERM_STATUS -ne 0 ]; then - log_success "Verzeichnisberechtigungen wurden erfolgreich korrigiert" -elif [ $PERM_STATUS_AFTER -ne 0 ] && [ $PERM_STATUS -ne 0 ]; then - log_error "Es bestehen weiterhin Probleme mit Verzeichnisberechtigungen" -fi - -if [ $VENV_STATUS_AFTER -eq 0 ] && [ $VENV_STATUS -ne 0 ]; then - log_success "Virtuelle Python-Umgebung wurde erfolgreich repariert" -elif [ $VENV_STATUS_AFTER -ne 0 ] && [ $VENV_STATUS -ne 0 ]; then - log_error "Es bestehen weiterhin Probleme mit der virtuellen Python-Umgebung" -fi - -if [ $PYMONGO_STATUS_AFTER -eq 0 ] && [ $PYMONGO_STATUS -ne 0 ]; then - log_success "PyMongo/BSON-Konflikte wurden erfolgreich behoben" -elif [ $PYMONGO_STATUS_AFTER -ne 0 ] && [ $PYMONGO_STATUS -ne 0 ]; then - log_error "Es bestehen weiterhin PyMongo/BSON-Konflikte" -fi - -if [ $SERVICES_STATUS_AFTER -eq 0 ] && [ $SERVICES_STATUS -ne 0 ]; then - log_success "Systemdienste wurden erfolgreich gestartet" -elif [ $SERVICES_STATUS_AFTER -ne 0 ] && [ $SERVICES_STATUS -ne 0 ]; then - log_warning "Es laufen weiterhin nicht alle Systemdienste" -fi - -# Final step: Summary -log_section "REPARATUR ABGESCHLOSSEN" - -log_message "Die All-in-One Reparatur wurde abgeschlossen." - -if [ "$AUTO_MODE" = true ]; then - log_success "Automatischer Reparaturmodus: Alle erkannten Probleme wurden behoben" - log_message "Ein detaillierter Bericht wurde in logs/auto_fix_report.log gespeichert" - - # Restart services automatically in auto mode - if command -v systemctl >/dev/null 2>&1; then - log_message "Starte Dienste automatisch neu..." - - # Restart MongoDB if it was stopped - if ! systemctl is-active --quiet mongodb 2>/dev/null; then - systemctl restart mongodb 2>/dev/null - fi - - # Restart Inventarsystem services - if systemctl list-unit-files | grep -q inventarsystem-gunicorn.service; then - systemctl restart inventarsystem-gunicorn.service 2>/dev/null - fi - - if systemctl list-unit-files | grep -q inventarsystem-nginx.service; then - systemctl restart inventarsystem-nginx.service 2>/dev/null - fi - - log_success "Dienste wurden neu gestartet" - fi -else - log_message "Nächste Schritte:" - log_message "1. Starten Sie das System neu mit: sudo ./restart.sh" - log_message "2. Überprüfen Sie die Log-Dateien im logs-Verzeichnis" - log_message "3. Bei anhaltenden Problemen versuchen Sie: sudo ./rebuild-venv.sh" - log_message "" - log_message "Tipp: Um dieses Skript automatisch auszuführen, verwenden Sie:" - log_message "sudo ./fix-all.sh --setup-cron" -fi - -log_success "FERTIG" - -exit 0 diff --git a/init-admin.sh b/init-admin.sh deleted file mode 100755 index 6e77781..0000000 --- a/init-admin.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Initialize first admin user if database is empty -# Usage: ./init-admin.sh [username] [password] [first_name] [last_name] -# Default: admin / admin123456 / Admin / User - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -USERNAME="${1:-admin}" -PASSWORD="${2:-admin123456}" -FIRST_NAME="${3:-Admin}" -LAST_NAME="${4:-User}" - -# Wait for MongoDB to be ready -echo "Waiting for MongoDB to be ready..." -for i in {1..30}; do - if docker exec inventarsystem-mongodb mongosh --eval "db.adminCommand('ping')" >/dev/null 2>&1; then - echo "MongoDB is ready." - break - fi - if [ $i -eq 30 ]; then - echo "Error: MongoDB did not start in time." - exit 1 - fi - sleep 1 -done - -# Check if any users already exist -USER_COUNT=$(docker exec inventarsystem-mongodb mongosh --quiet --eval "db.users.countDocuments({})" Inventarsystem 2>/dev/null || echo "0") - -if [ "$USER_COUNT" -eq 0 ]; then - echo "Database is empty. Creating initial admin user: $USERNAME" - - # Hash the password - PASSWORD_HASH=$(python3 -c "import hashlib; print(hashlib.sha512(b'$PASSWORD').hexdigest())") - - # Insert admin user - docker exec inventarsystem-mongodb mongosh --eval " - db.users.insertOne({ - Username: '$USERNAME', - Password: '$PASSWORD_HASH', - Admin: true, - active_ausleihung: null, - name: '$FIRST_NAME', - last_name: '$LAST_NAME', - favorites: [] - }) - " Inventarsystem - - echo "✓ Admin user '$USERNAME' created successfully." - echo " Username: $USERNAME" - echo " Password: $PASSWORD" -else - echo "Database already has $USER_COUNT user(s). Skipping initialization." -fi diff --git a/install.sh b/install.sh index 7398ab0..7ee0693 100755 --- a/install.sh +++ b/install.sh @@ -176,7 +176,6 @@ Options: --skip-cleanup-old Do not run cleanup-old.sh after install --cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup --multitenant Use docker-compose-multitenant.yml (default) - --singletenant Use docker-compose.yml --legacy-db-name Legacy database name (default: $LEGACY_DB_NAME) --legacy-mongo-uri Legacy Mongo URI (default: $LEGACY_MONGO_URI) --legacy-system-dir Optional old system directory to remove after migration @@ -207,10 +206,6 @@ parse_args() { COMPOSE_FILE="docker-compose-multitenant.yml" shift ;; - --singletenant) - COMPOSE_FILE="docker-compose.yml" - shift - ;; --legacy-db-name) LEGACY_DB_NAME="$2" shift 2 diff --git a/manage-tenant.sh b/manage-tenant.sh index 4a05f5b..83b858c 100755 --- a/manage-tenant.sh +++ b/manage-tenant.sh @@ -124,6 +124,68 @@ EOF fi } +sync_tenant_port_map() { + local config_path="$CONFIG_FILE" + local env_file="$PWD/.docker-build.env" + local tenant_port_map + + tenant_port_map="$(python3 - <<'PY' "$config_path" +import json, os, sys +path = sys.argv[1] +if not os.path.isfile(path): + sys.exit(1) +with open(path, 'r', encoding='utf-8') as f: + cfg = json.load(f) +tenants = cfg.get('tenants', {}) +if not isinstance(tenants, dict): + sys.exit(0) +entries = [] +for tenant_id, conf in sorted(tenants.items()): + if isinstance(conf, dict): + port = conf.get('port') + if isinstance(port, (int, float)) or (isinstance(port, str) and str(port).strip().isdigit()): + entries.append(f"{int(port)}={tenant_id}") +print(','.join(entries)) +PY +)" + + if [ -z "$tenant_port_map" ]; then + if [ -f "$env_file" ]; then + sed -i '/^INVENTAR_TENANT_PORT_MAP=/d' "$env_file" + fi + return 0 + fi + + if [ ! -f "$env_file" ]; then + cat > "$env_file" <> "$env_file" + fi +} + +restart_app_container() { + local env_file="$PWD/.docker-build.env" + local compose_args=( -f "$PWD/docker-compose-multitenant.yml" ) + + if [ -f "$PWD/.docker-compose.runtime.override.yml" ]; then + compose_args+=( -f "$PWD/.docker-compose.runtime.override.yml" ) + fi + if [ -f "$env_file" ]; then + compose_args+=( --env-file "$env_file" ) + fi + + echo "Restarting app container to apply tenant configuration changes..." + docker compose "${compose_args[@]}" up -d --no-build --force-recreate app +} + remove_tenant_port() { local tenant_id="$1" local config_path="$CONFIG_FILE" @@ -239,6 +301,10 @@ case "$COMMAND" in fi register_tenant_port "$TENANT_ID" "$PORT_ARG" update_runtime_ports "$PORT_ARG" + sync_tenant_port_map + if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then + restart_app_container + fi fi echo "Adding new tenant '$TENANT_ID'..." @@ -331,8 +397,16 @@ print(f'Database for tenant {sys.argv[1]} dropped.') if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then if [ -n "$port_to_remove" ]; then remove_runtime_port "$port_to_remove" + sync_tenant_port_map + if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then + restart_app_container + fi echo "Removed tenant '$TENANT_ID' from config.json and cleaned runtime port $port_to_remove." else + sync_tenant_port_map + if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then + restart_app_container + fi echo "Removed tenant '$TENANT_ID' from config.json. No port mapping was present." fi else diff --git a/manage-version.sh b/manage-version.sh deleted file mode 100644 index 4d7a3a3..0000000 --- a/manage-version.sh +++ /dev/null @@ -1,337 +0,0 @@ -#!/bin/bash - -# Standalone version manager for this repo -# Supports pinning to a commit/tag/branch, one-time use, clearing the lock, -# applying the lock, listing refs, and showing status. Optional restart. - -set -euo pipefail - -PROJECT_DIR="$(dirname "$(readlink -f "$0")")" -cd "$PROJECT_DIR" - -LOCK_FILE="$PROJECT_DIR/.version-lock" -RESTART=false -FORCE=false -BACKUP_BASE_DIR="/var/backups" -LOG_DIR="$PROJECT_DIR/logs" -VM_LOG="$LOG_DIR/version_manager.log" - -# Directories/files to preserve across version switches -# Data paths we want to restore from backup after a version switch -DATA_PATHS=( - "certs" - "Images" - "logs" - "Web/uploads" - "Web/thumbnails" - "Web/QRCodes" - "Web/previews" -) - -# Ensure log directory exists -mkdir -p "$LOG_DIR" >/dev/null 2>&1 || true -chmod 777 "$LOG_DIR" >/dev/null 2>&1 || true - -log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$VM_LOG"; } -err() { echo "ERROR: $*" | tee -a "$VM_LOG" >&2; } - -usage() { - cat < [--restart] [--force] Persistently lock to a commit/tag/branch and deploy it - $0 use [--restart] [--force] Use ref once (no persistence) and deploy it - $0 clear [--restart] [--force] Remove lock, switch back to main, pull latest - $0 apply [--restart] [--force] Re-apply the current lock (if any) - $0 status Show current commit and lock state - $0 list [--tags|--commits] List tags or recent commits (default: tags) - -Notes: - - can be a tag name, branch name, or full/short commit hash - - --restart calls ./restart.sh after switching - - --force discards local changes (git reset --hard) if needed -USAGE -} - -ensure_git_ready() { - # Avoid "dubious ownership" errors - git config --global --add safe.directory "$PROJECT_DIR" >/dev/null 2>&1 || true - # Fetch refs and tags - git fetch --all --tags --prune -} - -ensure_clean_or_force() { - if ! git diff --quiet || ! git diff --cached --quiet; then - if [ "$FORCE" = true ]; then - log "Discarding local changes (force)" - git reset --hard - git clean -fdx >/dev/null 2>&1 || true - else - err "Working tree has local changes. Re-run with --force to discard." - exit 2 - fi - fi -} - -restart_if_requested() { - if [ "$RESTART" = true ]; then - if [ -x "$PROJECT_DIR/restart.sh" ]; then - log "Restarting services..." - "$PROJECT_DIR/restart.sh" - else - log "restart.sh not found or not executable; skipping restart" - fi - fi -} - -# Create a full backup before switching versions (files + DB CSVs) -LAST_BACKUP_ARCHIVE="" -LAST_BACKUP_DIRNAME="" -create_pre_switch_backup() { - local ts backup_name backup_dir backup_archive - ts=$(date +"%Y-%m-%d_%H-%M-%S") - backup_name="Inventarsystem-pre-switch-$ts" - backup_dir="$BACKUP_BASE_DIR/$backup_name" - backup_archive="$BACKUP_BASE_DIR/$backup_name.tar.gz" - - log "Creating pre-switch backup at $backup_archive" - sudo mkdir -p "$backup_dir" || { err "Failed to create backup directory"; return 1; } - - # Copy project files (exclude .venv to reduce size, keep .git for reference) - log "Copying project files to backup directory..." - sudo rsync -a --delete \ - --exclude='.venv' \ - --exclude='__pycache__' \ - --exclude='*.pyc' \ - "$PROJECT_DIR/" "$backup_dir/" || log "Warning: rsync file copy encountered issues" - - # Database CSV backup using existing helper - log "Backing up MongoDB to CSVs..." - sudo mkdir -p "$backup_dir/mongodb_backup" || true - if [ -x "$PROJECT_DIR/run-backup.sh" ]; then - if sudo -E "$PROJECT_DIR/run-backup.sh" --db Inventarsystem --uri mongodb://localhost:27017/ --out "$backup_dir/mongodb_backup" >> "$VM_LOG" 2>&1; then - log "Database backup completed" - else - err "Database backup failed; continuing with file backup only" - fi - else - log "run-backup.sh not found; skipping DB backup" - fi - - # Compress backup - log "Compressing backup archive..." - if sudo tar -czf "$backup_archive" -C "$BACKUP_BASE_DIR" "$backup_name"; then - log "Backup archived to $backup_archive" - sudo rm -rf "$backup_dir" || true - # Set readable permissions - sudo chmod 644 "$backup_archive" || true - # Export for subsequent restore step - LAST_BACKUP_ARCHIVE="$backup_archive" - LAST_BACKUP_DIRNAME="$backup_name" - else - err "Failed to create compressed backup archive" - return 1 - fi -} - -# Restore key data from the backup archive into current working tree -restore_from_backup_archive() { - if [ -z "$LAST_BACKUP_ARCHIVE" ] || [ ! -f "$LAST_BACKUP_ARCHIVE" ]; then - log "No backup archive available to restore data from; skipping data restore" - return 0 - fi - local tmpdir - tmpdir=$(mktemp -d /tmp/inventarsystem_restore_XXXXXX) - log "Extracting backup archive $LAST_BACKUP_ARCHIVE to $tmpdir" - if ! sudo tar -xzf "$LAST_BACKUP_ARCHIVE" -C "$tmpdir"; then - err "Failed to extract backup archive; skipping data restore" - rm -rf "$tmpdir" || true - return 1 - fi - local src_root="$tmpdir/$LAST_BACKUP_DIRNAME" - # Some backups might extract without top-level dir; fallback - if [ ! -d "$src_root" ]; then - src_root="$tmpdir" - fi - log "Restoring data directories from backup..." - for rel in "${DATA_PATHS[@]}"; do - if [ -e "$src_root/$rel" ]; then - mkdir -p "$PROJECT_DIR/$(dirname "$rel")" 2>/dev/null || true - rsync -a "$src_root/$rel" "$PROJECT_DIR/$(dirname "$rel")/" >> "$VM_LOG" 2>&1 || log "Warning: failed to restore $rel" - log "Restored: $rel" - else - log "Not in backup (skipped): $rel" - fi - done - rm -rf "$tmpdir" || true -} - -# Ensure permissions on critical data directories -ensure_permissions() { - # Logs should be writable - mkdir -p "$PROJECT_DIR/logs" && chmod 777 "$PROJECT_DIR/logs" 2>/dev/null || true - # Web data directories readable/executable by server - for d in "$PROJECT_DIR/Web/uploads" "$PROJECT_DIR/Web/thumbnails" "$PROJECT_DIR/Web/QRCodes" "$PROJECT_DIR/Web/previews" "$PROJECT_DIR/Images"; do - [ -d "$d" ] && chmod -R 755 "$d" 2>/dev/null || true - done - # SSL cert perms (key must be 600) - if [ -f "$PROJECT_DIR/certs/inventarsystem.key" ]; then - chmod 600 "$PROJECT_DIR/certs/inventarsystem.key" 2>/dev/null || true - fi -} - -checkout_ref() { - local ref="$1" - - # Try tag - if git show-ref --verify --quiet "refs/tags/$ref"; then - git checkout -B locked-tag-"$ref" "tags/$ref" - return 0 - fi - # Try remote branch - if git show-ref --verify --quiet "refs/remotes/origin/$ref"; then - git checkout -B locked-branch-"$ref" "origin/$ref" - return 0 - fi - # Try local branch - if git show-ref --verify --quiet "refs/heads/$ref"; then - git checkout "$ref" - return 0 - fi - # Try commit-ish - if git rev-parse --verify --quiet "$ref^{commit}" >/dev/null; then - git checkout -B locked-commit "$ref" - return 0 - fi - - err "Could not resolve ref '$ref' to tag/branch/commit" - exit 3 -} - -deploy_main_latest() { - ensure_clean_or_force - if git show-ref --verify --quiet refs/heads/main; then - git checkout main - else - git checkout -B main origin/main - fi - git pull --ff-only -} - -cmd_status() { - local head_commit - head_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") - echo "Current commit: $head_commit" - if [ -f "$LOCK_FILE" ]; then - echo "Version lock: $(cat "$LOCK_FILE")" - else - echo "Version lock: none (tracking main)" - fi -} - -cmd_list() { - local mode="tags" - for a in "$@"; do - case "$a" in - --tags) mode="tags" ;; - --commits) mode="commits" ;; - esac - done - ensure_git_ready - if [ "$mode" = "tags" ]; then - git tag --list | sort -V - else - git --no-pager log --oneline -n 30 - fi -} - -cmd_pin() { - local ref="$1" - ensure_git_ready - # Always produce a backup before switching - create_pre_switch_backup || log "Backup step had issues; proceeding with caution" - ensure_clean_or_force - echo "$ref" > "$LOCK_FILE" - checkout_ref "$ref" - # Restore data from the backup archive - restore_from_backup_archive - ensure_permissions - log "Pinned to: $ref (commit $(git rev-parse --short HEAD))" - restart_if_requested -} - -cmd_use() { - local ref="$1" - ensure_git_ready - create_pre_switch_backup || log "Backup step had issues; proceeding with caution" - ensure_clean_or_force - checkout_ref "$ref" - restore_from_backup_archive - ensure_permissions - log "Using (one-time): $ref (commit $(git rev-parse --short HEAD))" - restart_if_requested -} - -cmd_clear() { - ensure_git_ready - create_pre_switch_backup || log "Backup step had issues; proceeding with caution" - [ -f "$LOCK_FILE" ] && rm -f "$LOCK_FILE" - deploy_main_latest - restore_from_backup_archive - ensure_permissions - log "Cleared version lock; now on main @ $(git rev-parse --short HEAD)" - restart_if_requested -} - -cmd_apply() { - ensure_git_ready - create_pre_switch_backup || log "Backup step had issues; proceeding with caution" - if [ -f "$LOCK_FILE" ]; then - ensure_clean_or_force - local ref - ref=$(cat "$LOCK_FILE") - checkout_ref "$ref" - log "Applied lock: $ref (commit $(git rev-parse --short HEAD))" - else - log "No lock present; keeping main" - deploy_main_latest - fi - restore_from_backup_archive - ensure_permissions - restart_if_requested -} - -# Parse global flags that can appear after subcommand too -parse_tail_flags() { - for a in "$@"; do - case "$a" in - --restart) RESTART=true ;; - --force) FORCE=true ;; - esac - done -} - -main() { - local cmd="${1:-}"; shift || true - case "$cmd" in - pin) - [ $# -ge 1 ] || { usage; exit 1; } - local ref="$1"; shift; parse_tail_flags "$@"; cmd_pin "$ref" ;; - use) - [ $# -ge 1 ] || { usage; exit 1; } - local ref="$1"; shift; parse_tail_flags "$@"; cmd_use "$ref" ;; - clear) - parse_tail_flags "$@"; cmd_clear ;; - apply) - parse_tail_flags "$@"; cmd_apply ;; - status) - cmd_status ;; - list) - cmd_list "$@" ;; - --help|-h|help|"") - usage ;; - *) - err "Unknown command: $cmd"; usage; exit 1 ;; - esac -} - -main "$@" diff --git a/migrate-to-multitenant.sh b/migrate-to-multitenant.sh deleted file mode 100644 index 3197ca2..0000000 --- a/migrate-to-multitenant.sh +++ /dev/null @@ -1,426 +0,0 @@ -#!/bin/bash - -############################################################################## -# Multi-Tenant Migration Script -# -# Automatisierte Konvertierung einer Single-Instance App zu Multi-Tenant -# Führt folgende Operationen durch: -# 1. Backup der Original-Dateien -# 2. Import von Tenant-Modulen -# 3. Anpassung von app.py -# 4. Konfiguration von Redis -# 5. Validierung -# -# Usage: bash migrate-to-multitenant.sh [dry-run] -############################################################################## - -set -e - -# Farben für Terminal Output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -WEB_DIR="$PROJECT_ROOT/Web" -BACKUP_DIR="$PROJECT_ROOT/.migration-backup-$(date +%Y%m%d-%H%M%S)" -DRY_RUN="${1:-}" - -log_info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -log_success() { - echo -e "${GREEN}[✓]${NC} $1" -} - -log_warning() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -check_prerequisites() { - log_info "Checking prerequisites..." - - # Check Python - if ! command -v python3 &> /dev/null; then - log_error "Python 3 not found" - return 1 - fi - log_success "Python 3 found: $(python3 --version)" - - # Check Docker - if ! command -v docker &> /dev/null; then - log_warning "Docker not found (will be needed for deployment)" - else - log_success "Docker found: $(docker --version)" - fi - - # Check Flask app - if [[ ! -f "$WEB_DIR/app.py" ]]; then - log_error "Web/app.py not found" - return 1 - fi - log_success "Flask app found at $WEB_DIR/app.py" - - return 0 -} - -create_backups() { - log_info "Creating backups..." - - if [[ "$DRY_RUN" == "dry-run" ]]; then - log_info "[DRY-RUN] Would backup to: $BACKUP_DIR" - return 0 - fi - - mkdir -p "$BACKUP_DIR" - - # Backup critical files - cp "$WEB_DIR/app.py" "$BACKUP_DIR/app.py.bak" - cp "$WEB_DIR/settings.py" "$BACKUP_DIR/settings.py.bak" 2>/dev/null || true - cp "docker-compose.yml" "$BACKUP_DIR/docker-compose.yml.bak" 2>/dev/null || true - - log_success "Backups created at: $BACKUP_DIR" -} - -check_tenant_modules() { - log_info "Checking tenant modules..." - - local missing=0 - - for module in tenant session_manager query_cache; do - if [[ ! -f "$WEB_DIR/${module}.py" ]]; then - log_warning "Missing module: $WEB_DIR/${module}.py" - ((missing++)) - else - log_success "Module found: $module.py" - fi - done - - if [[ $missing -gt 0 ]]; then - log_error "Missing $missing required modules. Ensure they are created first." - return 1 - fi - - return 0 -} - -check_docker_files() { - log_info "Checking Docker configuration files..." - - local missing=0 - - if [[ ! -f "docker-compose-multitenant.yml" ]]; then - log_warning "Missing docker-compose-multitenant.yml" - ((missing++)) - else - log_success "docker-compose-multitenant.yml found" - fi - - if [[ ! -f "docker/nginx/multitenant.conf" ]]; then - log_warning "Missing docker/nginx/multitenant.conf" - ((missing++)) - else - log_success "docker/nginx/multitenant.conf found" - fi - - if [[ $missing -gt 0 ]]; then - log_warning "Missing $missing Docker files (needed for deployment)" - fi - - return 0 -} - -update_app_py() { - log_info "Updating app.py with Multi-Tenant imports..." - - if [[ "$DRY_RUN" == "dry-run" ]]; then - log_info "[DRY-RUN] Would add Multi-Tenant imports to app.py" - return 0 - fi - - # Check if already updated - if grep -q "from tenant import" "$WEB_DIR/app.py"; then - log_warning "app.py already contains Multi-Tenant imports (skipping)" - return 0 - fi - - # Add imports after other data_protection imports - local imports_section=$(python3 -c " -import re -with open('$WEB_DIR/app.py', 'r') as f: - content = f.read() - -# Find the line after data_protection imports -if 'from data_protection import' in content: - lines = content.split('\n') - for i, line in enumerate(lines): - if 'from data_protection import' in line: - # Find the end of this import block - j = i + 1 - while j < len(lines) and (lines[j].startswith(' ') or lines[j].strip() == ''): - j += 1 - # Insert new imports before settings import - print(j) - break -else: - print(-1) -" 2>/dev/null) - - if [[ $imports_section -eq -1 ]]; then - log_warning "Could not find insertion point for Multi-Tenant imports" - log_info "Please manually add the following imports to app.py:" - cat << 'EOF' - -# Multi-Tenant Imports -from tenant import get_tenant_context, require_tenant, get_tenant_db -from session_manager import create_redis_session_interface -from query_cache import get_cache_manager, cached_query, invalidate_cache -EOF - return 1 - fi - - log_success "Multi-Tenant imports prepared" - return 0 -} - -update_requirements() { - log_info "Checking requirements.txt for Redis..." - - if [[ "$DRY_RUN" == "dry-run" ]]; then - log_info "[DRY-RUN] Would update requirements.txt" - return 0 - fi - - # Check both root and Web/requirements.txt - for req_file in requirements.txt Web/requirements.txt; do - if [[ -f "$req_file" ]]; then - if ! grep -q "^redis" "$req_file"; then - log_info "Adding redis to $req_file..." - echo "redis>=7.0.0" >> "$req_file" - log_success "Added redis to $req_file" - else - log_success "$req_file already has redis" - fi - fi - done - - return 0 -} - -generate_migration_guide() { - log_info "Generating migration guide..." - - cat > "$BACKUP_DIR/MIGRATION_NOTES.md" << 'EOF' -# Multi-Tenant Migration Checklist - -## Automated Steps Completed -- [x] Backup created -- [x] Tenant modules verified -- [x] Docker files prepared -- [x] app.py imports prepared -- [x] requirements.txt updated - -## Manual Steps Required - -### 1. Update app.py Configuration (5 min) - -Add after `app = Flask(...)`: -```python -# Multi-Tenant Configuration -if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis': - try: - app.session_interface = create_redis_session_interface(app) - app.logger.info("Redis session backend enabled") - except Exception as e: - app.logger.warning(f"Redis session backend failed: {e}") -``` - -### 2. Add Health Check Endpoint (5 min) - -Add this route: -```python -@app.route('/health') -def health_check(): - try: - mongo = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) - mongo.admin.command('ping') - return {'status': 'healthy'}, 200 - except Exception as e: - return {'status': 'unhealthy'}, 503 -``` - -### 3. Update Database Access (10-30 min) - -Change existing routes to use: -```python -@require_tenant -def your_route(): - db = get_tenant_db(MongoClient(...)) - # Use db as usual -``` - -### 4. Setup DNS (5 min) - -Create wildcard DNS record: -``` -*.example.com IN A your-server-ip -``` - -### 5. Create SSL Certificate (10 min) - -```bash -# Let's Encrypt (recommended) -sudo certbot certonly --manual --preferred-challenges dns \ - -d "*.example.com" -d "example.com" - -# Copy to certs folder -cp /etc/letsencrypt/live/example.com/fullchain.pem certs/inventarsystem.crt -cp /etc/letsencrypt/live/example.com/privkey.pem certs/inventarsystem.key -``` - -### 6. Deploy Multi-Tenant - -```bash -# Install dependencies -pip install -r requirements.txt - -# Start Multi-Tenant deployment -docker-compose -f docker-compose-multitenant.yml up -d - -# Test -curl https://test.example.com/health -``` - -### 7. Verify (5 min) - -```bash -# Check logs -docker-compose logs app - -# Verify each tenant -curl https://schule1.example.com/debug/tenant -curl https://schule2.example.com/debug/tenant - -# Cache stats -curl https://schule1.example.com/debug/cache-stats -``` - -## Rollback Plan - -If something goes wrong: - -1. Stop Multi-Tenant deployment - ```bash - docker-compose -f docker-compose-multitenant.yml down - ``` - -2. Restore from backup - ```bash - cp .migration-backup-*/app.py Web/app.py - cp .migration-backup-*/settings.py Web/settings.py - ``` - -3. Restart original deployment - ```bash - docker-compose up -d - ``` - -## Performance Expectations - -- Memory per instance: 100-150MB (was 200MB) -- Response time: -30% (Redis caching) -- Max concurrent users: 3x increase -- Database load: -70% (query caching) - -## Support - -- Check logs: `docker-compose -f docker-compose-multitenant.yml logs -f app` -- Debug tenant: `curl https://your-tenant.com/debug/tenant` -- See MULTITENANT_DEPLOYMENT.md for detailed guide -EOF - - log_success "Migration guide created at: $BACKUP_DIR/MIGRATION_NOTES.md" -} - -validate_setup() { - log_info "Validating setup..." - - # Check Python syntax of tenant modules - for module in tenant session_manager query_cache; do - module_file="$WEB_DIR/${module}.py" - if [[ -f "$module_file" ]]; then - if python3 -m py_compile "$module_file" 2>/dev/null; then - log_success "Module syntax valid: $module.py" - else - log_error "Syntax error in $module.py" - return 1 - fi - fi - done - - return 0 -} - -print_summary() { - cat << EOF - -${GREEN}═══════════════════════════════════════════════════════════${NC} -${GREEN} Multi-Tenant Migration Summary${NC} -${GREEN}═══════════════════════════════════════════════════════════${NC} - -${GREEN}✓ Prerequisites checked${NC} -${GREEN}✓ Backups created${NC} -${GREEN}✓ Tenant modules verified${NC} -${GREEN}✓ Docker files prepared${NC} -${GREEN}✓ app.py imports prepared${NC} -${GREEN}✓ requirements.txt updated${NC} - -${YELLOW}Next Steps:${NC} -1. Review migration guide: $BACKUP_DIR/MIGRATION_NOTES.md -2. Manually update app.py (see guide above) -3. Setup DNS wildcard record -4. Create SSL certificate -5. Deploy: docker-compose -f docker-compose-multitenant.yml up -d - -${BLUE}Documentation:${NC} -- MULTITENANT_DEPLOYMENT.md - Complete deployment guide -- MULTITENANT_INTEGRATION.md - Code integration examples -- $BACKUP_DIR/MIGRATION_NOTES.md - Step-by-step checklist - -${GREEN}═══════════════════════════════════════════════════════════${NC} - -EOF -} - -main() { - log_info "Multi-Tenant Migration Starting..." - - if [[ "$DRY_RUN" == "dry-run" ]]; then - log_warning "Running in DRY-RUN mode - no changes will be made" - fi - - check_prerequisites || exit 1 - create_backups - check_tenant_modules || exit 1 - check_docker_files - update_app_py || true - update_requirements - validate_setup || exit 1 - generate_migration_guide - - print_summary - - if [[ "$DRY_RUN" == "dry-run" ]]; then - log_warning "DRY-RUN completed - no changes were made" - log_info "Run without 'dry-run' parameter to execute migration" - fi -} - -main "$@" diff --git a/rebuild-venv.sh b/rebuild-venv.sh deleted file mode 100755 index b5a3687..0000000 --- a/rebuild-venv.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash - -# This script completely rebuilds the virtual environment -# It should be run as root/sudo - -# Get the script directory -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -VENV_DIR="$SCRIPT_DIR/.venv" -BACKUP_DIR="$SCRIPT_DIR/.venv_backup_$(date +%Y%m%d%H%M%S)" - -# Function to log messages -log_message() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" -} - -log_message "Starting virtual environment rebuild" - -# Backup existing virtual environment if it exists -if [ -d "$VENV_DIR" ]; then - log_message "Backing up existing virtual environment to $BACKUP_DIR" - mv "$VENV_DIR" "$BACKUP_DIR" || { - log_message "ERROR: Failed to backup existing virtual environment. Proceeding without backup." - rm -rf "$VENV_DIR" - } -fi - -# Create logs directory with proper permissions -log_message "Ensuring logs directory exists with proper permissions" -mkdir -p "$SCRIPT_DIR/logs" -chmod 777 "$SCRIPT_DIR/logs" || log_message "WARNING: Could not set permissions on logs directory" - -# Create a new virtual environment -log_message "Creating new virtual environment" -python3 -m venv "$VENV_DIR" || { - log_message "ERROR: Failed to create virtual environment. Trying with system packages..." - python3 -m venv "$VENV_DIR" --system-site-packages || { - log_message "ERROR: Failed to create virtual environment even with system packages. Exiting." - exit 1 - } -} - -# Set proper permissions -log_message "Setting permissions for new virtual environment" -chmod -R 755 "$VENV_DIR" || log_message "WARNING: Could not set permissions on virtual environment" -find "$VENV_DIR" -type d -exec chmod 755 {} \; 2>/dev/null -find "$VENV_DIR/bin" -type f -exec chmod +x {} \; 2>/dev/null - -# Activate the virtual environment -log_message "Activating virtual environment" -source "$VENV_DIR/bin/activate" || { - log_message "ERROR: Failed to activate virtual environment" - exit 1 -} - -# Install packages -log_message "Upgrading pip" -pip install --upgrade pip || log_message "WARNING: Failed to upgrade pip" - -log_message "Installing pymongo" -# First ensure bson is removed to avoid conflicts -pip uninstall -y bson || log_message "WARNING: Failed to uninstall bson (may not exist)" -# Check if bson directory exists after uninstall and remove it if necessary -if [ -d "$VENV_DIR/lib/python3.12/site-packages/bson" ]; then - log_message "Force removing bson directory" - rm -rf "$VENV_DIR/lib/python3.12/site-packages/bson" -fi -# Now install pymongo -pip install pymongo==4.6.3 || log_message "WARNING: Failed to install pymongo" -# Verify pymongo installation -python -c "import pymongo; print(f'PyMongo version: {pymongo.__version__}')" && log_message "✓ PyMongo installed correctly" || log_message "WARNING: PyMongo verification failed" - -# Install other requirements if they exist -if [ -f "$SCRIPT_DIR/requirements.txt" ]; then - log_message "Installing packages from requirements.txt (excluding bson)" - # Create a modified requirements file without bson - grep -v "^bson" "$SCRIPT_DIR/requirements.txt" > "$SCRIPT_DIR/requirements_filtered.txt" - # Also remove pymongo since we already installed it - grep -v "^pymongo" "$SCRIPT_DIR/requirements_filtered.txt" > "$SCRIPT_DIR/requirements_no_conflicts.txt" - pip install -r "$SCRIPT_DIR/requirements_no_conflicts.txt" || log_message "WARNING: Failed to install some packages from requirements.txt" - rm -f "$SCRIPT_DIR/requirements_filtered.txt" "$SCRIPT_DIR/requirements_no_conflicts.txt" - - log_message "Verifying pymongo installation after package installation..." - # Check if the bson directory from the standalone package still exists and remove it - if [ -d "$VENV_DIR/lib/python3.12/site-packages/bson" ]; then - log_message "Found standalone bson package, removing it..." - rm -rf "$VENV_DIR/lib/python3.12/site-packages/bson" - # Reinstall pymongo to ensure it's correctly configured - pip install --force-reinstall pymongo==4.6.3 - fi -else - log_message "No requirements.txt found, installing essential packages manually" - pip install flask werkzeug gunicorn pillow qrcode apscheduler python-dateutil pytz requests || { - log_message "WARNING: Failed to install some essential packages" - } -fi - -# Deactivate virtual environment -deactivate - -# Verify the virtual environment works correctly -log_message "Verifying virtual environment..." -if "$VENV_DIR/bin/python" -c "import pymongo, flask, gunicorn, requests, PIL, apscheduler; exit(0)" 2>/dev/null; then - log_message "✓ Virtual environment verification successful" - - # Clean up backup if everything works - if [ -d "$BACKUP_DIR" ]; then - log_message "Removing backup virtual environment since verification was successful" - rm -rf "$BACKUP_DIR" - log_message "✓ Backup environment removed" - fi -else - log_message "WARNING: Virtual environment verification failed" - log_message "Keeping backup at $BACKUP_DIR in case it's needed for recovery" -fi - -log_message "Virtual environment rebuild complete" -log_message "You can now run the restart.sh script" - -exit 0 diff --git a/restart.sh b/restart.sh index 0eed1d2..05734de 100755 --- a/restart.sh +++ b/restart.sh @@ -46,14 +46,16 @@ services: image: $app_image build: null EOF - if [ ${#ports[@]} -gt 1 ]; then + if [ ${#ports[@]} -gt 0 ]; then cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <> "$RUNTIME_COMPOSE_OVERRIDE_FILE" </dev/null 2>&1; then - SUDO="sudo" -fi - -DOCKER_COMPOSE=() - -cleanup() { - rm -rf "$WORK_DIR" >/dev/null 2>&1 || true -} -trap cleanup EXIT - -mkdir -p "$LOG_DIR" - -log() { - local msg="$1" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $msg" | tee -a "$LOG_FILE" -} - -show_help() { - cat < [options] - $0 --date [options] - $0 --list - -Options: - --source Backup source path. - Supported: - - old folder with CSV files - - old Inventarsystem-*.tar.gz backup - - new mongodb-*.archive.gz backup - - folder containing mongodb-*.archive.gz - --date Resolve source from known backup locations. - - YYYY-MM-DD: checks backups/YYYY-MM-DD and /var/backups/Inventarsystem-YYYY-MM-DD(.tar.gz) - - latest: newest from local/system backup roots - --drop-database Drop target DB before import (recommended for full restore) - --restart-services Restart stack after restore - --multitenant Use docker-compose-multitenant.yml (default) - --singletenant Use docker-compose.yml - --list List detected backup candidates - --help Show this help - -Notes: - - Always restores into current DB name: $DB_NAME - - Supports both legacy CSV and new archive backups - - Normalizes collection names to current structure (users, items, ausleihungen, filter_presets, settings) -EOF -} - -setup_compose() { - if docker compose version >/dev/null 2>&1 && docker info >/dev/null 2>&1; then - DOCKER_COMPOSE=(docker compose -f "$SCRIPT_DIR/$COMPOSE_FILE") - return 0 - fi - - if [ -n "$SUDO" ] && $SUDO docker compose version >/dev/null 2>&1 && $SUDO docker info >/dev/null 2>&1; then - DOCKER_COMPOSE=($SUDO docker compose -f "$SCRIPT_DIR/$COMPOSE_FILE") - return 0 - fi - - log "ERROR: docker compose is not available" - exit 1 -} - -compose() { - "${DOCKER_COMPOSE[@]}" "$@" -} - -list_backups() { - echo "Detected backup candidates:" - - if [ -d "$BACKUP_ROOT_LOCAL" ]; then - find "$BACKUP_ROOT_LOCAL" -maxdepth 2 \( -type d -name "20*" -o -type f -name "*.archive.gz" -o -type f -name "Inventarsystem-*.tar.gz" \) 2>/dev/null | sort - fi - - if [ -d "$BACKUP_ROOT_SYSTEM" ]; then - find "$BACKUP_ROOT_SYSTEM" -maxdepth 1 \( -type d -name "Inventarsystem-*" -o -type f -name "Inventarsystem-*.tar.gz" \) 2>/dev/null | sort - fi -} - -resolve_latest_source() { - local latest - latest="$({ - if [ -d "$BACKUP_ROOT_LOCAL" ]; then - find "$BACKUP_ROOT_LOCAL" -maxdepth 2 \( -type d -name "20*" -o -type f -name "*.archive.gz" -o -type f -name "Inventarsystem-*.tar.gz" \) -printf '%T@ %p\n' 2>/dev/null - fi - if [ -d "$BACKUP_ROOT_SYSTEM" ]; then - find "$BACKUP_ROOT_SYSTEM" -maxdepth 1 \( -type d -name "Inventarsystem-*" -o -type f -name "Inventarsystem-*.tar.gz" \) -printf '%T@ %p\n' 2>/dev/null - fi - } | sort -nr | head -n1 | cut -d' ' -f2-)" - - if [ -z "$latest" ]; then - log "ERROR: no backup candidates found" - exit 1 - fi - - SOURCE_PATH="$latest" -} - -resolve_date_source() { - local d="$1" - - if [ "$d" = "latest" ]; then - resolve_latest_source - return - fi - - local p1="$BACKUP_ROOT_LOCAL/$d" - local p2="$BACKUP_ROOT_SYSTEM/Inventarsystem-$d" - local p3="$BACKUP_ROOT_SYSTEM/Inventarsystem-$d.tar.gz" - - if [ -e "$p1" ]; then - SOURCE_PATH="$p1" - elif [ -e "$p2" ]; then - SOURCE_PATH="$p2" - elif [ -e "$p3" ]; then - SOURCE_PATH="$p3" - else - log "ERROR: no backup found for date $d" - exit 1 - fi -} - -ensure_stack() { - log "Ensuring mongodb/app services are running..." - compose up -d mongodb app >/dev/null - - if ! compose ps --status running mongodb | grep -q mongodb; then - log "ERROR: mongodb container is not running" - exit 1 - fi - - if ! compose ps --status running app | grep -q app; then - log "ERROR: app container is not running" - exit 1 - fi -} - -stage_source() { - local src="$1" - - if [ ! -e "$src" ]; then - log "ERROR: source path does not exist: $src" - exit 1 - fi - - if [ -f "$src" ] && [[ "$src" == *.tar.gz ]]; then - log "Extracting tar backup: $src" - tar -xzf "$src" -C "$WORK_DIR" - STAGED_PATH="$WORK_DIR" - return - fi - - if [ -f "$src" ]; then - cp -f "$src" "$WORK_DIR/" - STAGED_PATH="$WORK_DIR" - return - fi - - if [ -d "$src" ]; then - STAGED_PATH="$src" - return - fi - - log "ERROR: unsupported source type: $src" - exit 1 -} - -find_archive_file() { - local root="$1" - find "$root" -type f \( -name "*.archive.gz" -o -name "*.archive" \) 2>/dev/null | head -n1 || true -} - -find_best_csv_dir() { - local root="$1" - local best_dir="" - local best_count=0 - - while IFS= read -r dir; do - [ -z "$dir" ] && continue - local count - count="$(find "$dir" -maxdepth 1 -type f -name '*.csv' | wc -l | tr -d ' ')" - if [ "$count" -gt "$best_count" ]; then - best_count="$count" - best_dir="$dir" - fi - done < <(find "$root" -type f -name '*.csv' -printf '%h\n' 2>/dev/null | sort -u) - - if [ "$best_count" -gt 0 ]; then - echo "$best_dir" - fi -} - -normalize_structure() { - local py="$WORK_DIR/normalize_db.py" - - cat > "$py" <<'PY' -from pymongo import MongoClient -import os - -db_name = os.getenv("DB_NAME", "Inventarsystem") -host = os.getenv("INVENTAR_MONGODB_HOST", "mongodb") -port = int(os.getenv("INVENTAR_MONGODB_PORT", "27017")) - -client = MongoClient(host, port) -db = client[db_name] - -rename_map = { - "user": "users", - "item": "items", - "ausleihung": "ausleihungen", - "filter_preset": "filter_presets", - "preset": "filter_presets", - "setting": "settings", -} - -for old, new in rename_map.items(): - if old not in db.list_collection_names(): - continue - - if new not in db.list_collection_names(): - db[old].rename(new) - print(f"Renamed collection {old} -> {new}") - continue - - moved = 0 - for doc in db[old].find({}): - db[new].insert_one(doc) - moved += 1 - db[old].drop() - print(f"Merged {moved} docs from {old} into {new}") - -print("Final collections:", sorted(db.list_collection_names())) -PY - - local app_cid - app_cid="$(compose ps -q app)" - - $SUDO docker cp "$py" "$app_cid:/tmp/normalize_db.py" - compose exec -T app env DB_NAME="$DB_NAME" python /tmp/normalize_db.py -} - -import_archive() { - local archive_file="$1" - local mongo_cid - - mongo_cid="$(compose ps -q mongodb)" - if [ -z "$mongo_cid" ]; then - log "ERROR: could not resolve mongodb container id" - exit 1 - fi - - log "Importing archive backup: $archive_file" - $SUDO docker cp "$archive_file" "$mongo_cid:/tmp/restore.archive.gz" - - local drop_flag="" - if [ "$DROP_DATABASE" = true ]; then - drop_flag="--drop" - fi - - compose exec -T mongodb sh -lc "mongorestore --archive=/tmp/restore.archive.gz --gzip $drop_flag" - compose exec -T mongodb rm -f /tmp/restore.archive.gz >/dev/null 2>&1 || true - - normalize_structure -} - -import_csv() { - local csv_dir="$1" - local app_cid py - - app_cid="$(compose ps -q app)" - if [ -z "$app_cid" ]; then - log "ERROR: could not resolve app container id" - exit 1 - fi - - log "Importing CSV backup directory: $csv_dir" - compose exec -T app sh -lc "rm -rf /tmp/restore_csv && mkdir -p /tmp/restore_csv" - $SUDO docker cp "$csv_dir/." "$app_cid:/tmp/restore_csv" - - py="$WORK_DIR/import_csv.py" - cat > "$py" <<'PY' -import os -import csv -import ast -import re -from pymongo import MongoClient -from bson.objectid import ObjectId - -DB_NAME = os.getenv("DB_NAME", "Inventarsystem") -CSV_DIR = os.getenv("CSV_DIR", "/tmp/restore_csv") -DROP_DATABASE = os.getenv("DROP_DATABASE", "false").lower() == "true" -HOST = os.getenv("INVENTAR_MONGODB_HOST", "mongodb") -PORT = int(os.getenv("INVENTAR_MONGODB_PORT", "27017")) - -COLLECTION_MAP = { - "users": "users", - "user": "users", - "items": "items", - "item": "items", - "ausleihungen": "ausleihungen", - "ausleihung": "ausleihungen", - "filter_presets": "filter_presets", - "filter_preset": "filter_presets", - "settings": "settings", - "setting": "settings", -} - -BOOL_FIELDS = {"Admin", "Verfuegbar", "enabled"} -INT_FIELDS = {"Anschaffungskosten", "filter_num"} -OID_LIKE_FIELDS = {"_id", "Item"} - -client = MongoClient(HOST, PORT) -db = client[DB_NAME] - -if DROP_DATABASE: - client.drop_database(DB_NAME) - db = client[DB_NAME] - - -def parse_scalar(key, value): - if value is None: - return None - - value = value.strip() - if value == "": - return None - - if key in BOOL_FIELDS: - lowered = value.lower() - if lowered == "true": - return True - if lowered == "false": - return False - - if key in INT_FIELDS: - try: - return int(value) - except Exception: - pass - - if key in OID_LIKE_FIELDS and re.fullmatch(r"[0-9a-fA-F]{24}", value): - return ObjectId(value) - - if value.startswith("[") and value.endswith("]"): - try: - return ast.literal_eval(value) - except Exception: - pass - - return value - - -def load_csv_file(csv_path): - docs = [] - with open(csv_path, "r", newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - doc = {} - for key, value in row.items(): - doc[key] = parse_scalar(key, value) - docs.append(doc) - return docs - - -csv_files = [ - os.path.join(CSV_DIR, f) - for f in sorted(os.listdir(CSV_DIR)) - if f.endswith(".csv") -] - -if not csv_files: - raise SystemExit(f"No CSV files found in {CSV_DIR}") - -for csv_file in csv_files: - base = os.path.splitext(os.path.basename(csv_file))[0] - coll = COLLECTION_MAP.get(base, base) - - docs = load_csv_file(csv_file) - db[coll].drop() - if docs: - db[coll].insert_many(docs) - print(f"Imported {len(docs)} docs into {coll} from {os.path.basename(csv_file)}") - -print("Collections after CSV import:", sorted(db.list_collection_names())) -PY - - $SUDO docker cp "$py" "$app_cid:/tmp/import_csv.py" - compose exec -T app env DB_NAME="$DB_NAME" CSV_DIR="/tmp/restore_csv" DROP_DATABASE="$DROP_DATABASE" python /tmp/import_csv.py - - normalize_structure -} - -print_counts() { - local py="$WORK_DIR/print_counts.py" - - cat > "$py" <<'PY' -from pymongo import MongoClient -import os - -db_name = os.getenv("DB_NAME", "Inventarsystem") -host = os.getenv("INVENTAR_MONGODB_HOST", "mongodb") -port = int(os.getenv("INVENTAR_MONGODB_PORT", "27017")) - -client = MongoClient(host, port) -db = client[db_name] - -for name in ["users", "items", "ausleihungen", "filter_presets", "settings"]: - try: - count = db[name].count_documents({}) - except Exception: - count = -1 - print(f"{name}: {count}") -PY - - local app_cid - app_cid="$(compose ps -q app)" - $SUDO docker cp "$py" "$app_cid:/tmp/print_counts.py" - - log "Post-restore collection counts:" - compose exec -T app env DB_NAME="$DB_NAME" python /tmp/print_counts.py | tee -a "$LOG_FILE" -} - -parse_args() { - while [ "$#" -gt 0 ]; do - case "$1" in - --source) - SOURCE_PATH="${2:-}" - shift 2 - ;; - --date) - BACKUP_DATE="${2:-}" - shift 2 - ;; - --drop-database) - DROP_DATABASE=true - shift - ;; - --restart-services) - RESTART_SERVICES=true - shift - ;; - --multitenant) - COMPOSE_FILE="docker-compose-multitenant.yml" - shift - ;; - --singletenant) - COMPOSE_FILE="docker-compose.yml" - shift - ;; - --list) - list_backups - exit 0 - ;; - --help) - show_help - exit 0 - ;; - *) - echo "Unknown argument: $1" - show_help - exit 1 - ;; - esac - done -} - -main() { - parse_args "$@" - setup_compose - - if [ -n "$SOURCE_PATH" ] && [ -n "$BACKUP_DATE" ]; then - log "ERROR: use either --source or --date, not both" - exit 1 - fi - - if [ -z "$SOURCE_PATH" ] && [ -n "$BACKUP_DATE" ]; then - resolve_date_source "$BACKUP_DATE" - fi - - if [ -z "$SOURCE_PATH" ]; then - log "ERROR: provide --source or --date " - show_help - exit 1 - fi - - ensure_stack - - local staged archive_file csv_dir - stage_source "$SOURCE_PATH" - staged="$STAGED_PATH" - archive_file="$(find_archive_file "$staged")" - - if [ -n "$archive_file" ]; then - import_archive "$archive_file" - print_counts - else - csv_dir="$(find_best_csv_dir "$staged")" - if [ -z "$csv_dir" ]; then - log "ERROR: no supported backup data found under: $SOURCE_PATH" - log "Expected either *.archive.gz or CSV files" - exit 1 - fi - import_csv "$csv_dir" - print_counts - fi - - if [ "$RESTART_SERVICES" = true ]; then - log "Restarting services..." - "$SCRIPT_DIR/restart.sh" - fi - - log "Restore completed successfully into DB: $DB_NAME" -} - -main "$@" diff --git a/run-backup.sh b/run-backup.sh old mode 100755 new mode 100644 index 3ae792f..02e5094 --- a/run-backup.sh +++ b/run-backup.sh @@ -1,47 +1,4 @@ -#!/bin/bash -# Helper script to run the backup with proper Python environment - -# Get the script directory -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" - -# Default values if no arguments provided -if [ $# -eq 0 ]; then - # Define default values - DB="Inventarsystem" - URI="mongodb://localhost:27017/" - OUT="$SCRIPT_DIR/backups/$(date +%Y-%m-%d)" - - echo "No arguments provided. Using defaults:" - echo " --uri $URI" - echo " --db $DB" - echo " --out $OUT" - - # Create output directory if it doesn't exist - mkdir -p "$OUT" - - # Set the arguments - ARGS="--uri $URI --db $DB --out $OUT" -else - # Use provided arguments - ARGS="$@" -fi - -# Check if we're in a virtual environment -if [ -f "$SCRIPT_DIR/.venv/bin/python" ]; then - # Check if we have execute permissions for the virtual environment Python - if [ -x "$SCRIPT_DIR/.venv/bin/python" ]; then - # Use the virtual environment's Python - "$SCRIPT_DIR/.venv/bin/python" "$SCRIPT_DIR/Backup-DB.py" $ARGS || { - echo "Failed to execute with virtual environment Python. Trying system Python..." - python3 "$SCRIPT_DIR/Backup-DB.py" $ARGS - } - else - echo "Virtual environment found but Python is not executable. Using system Python instead." - echo "To fix virtual environment permissions, run: sudo ./rebuild-venv.sh" - python3 "$SCRIPT_DIR/Backup-DB.py" $ARGS - fi -else - # Use system Python - echo "No virtual environment found. Using system Python." - python3 "$SCRIPT_DIR/Backup-DB.py" $ARGS -fi +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/backup.sh" "$@" diff --git a/start.sh b/start.sh index 8eb695a..afa1210 100755 --- a/start.sh +++ b/start.sh @@ -37,7 +37,6 @@ Options: --no-cron Do not create or update cron jobs --with-cron Create/update cron jobs (default) --multitenant Use the multi-tenant architecture deployment - --singletenant Use the legacy single-tenant compose deployment -h, --help Show this help message EOF } @@ -57,10 +56,6 @@ parse_args() { COMPOSE_FILE="docker-compose-multitenant.yml" shift ;; - --singletenant) - COMPOSE_FILE="docker-compose.yml" - shift - ;; -h|--help) usage exit 0 @@ -584,15 +579,19 @@ EOF if [ -n "$HTTP_PORTS_VALUE" ]; then local ports_array - read -r -a ports_array <<<"$HTTP_PORTS_VALUE" - if [ "${#ports_array[@]}" -gt 1 ]; then + local ports_list + ports_list="${HTTP_PORTS_VALUE//,/ }" + read -r -a ports_array <<<"$ports_list" + if [ "${#ports_array[@]}" -gt 0 ]; then cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <