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" </dev/null; then
+ sed -i "s|^INVENTAR_TENANT_PORT_MAP=.*|INVENTAR_TENANT_PORT_MAP=$tenant_port_map|" "$env_file"
+ else
+ printf '\nINVENTAR_TENANT_PORT_MAP=%s\n' "$tenant_port_map" >> "$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" <]