Remove legacy scripts and single-tenant support
- Deleted the migration script `migrate-to-multitenant.sh` as it is no longer needed. - Removed `rebuild-venv.sh`, `restore.sh`, `run-backup.sh`, and other related scripts that are not compatible with the new multi-tenant architecture. - Updated `restart.sh`, `start.sh`, `stop.sh`, `update.sh`, and other scripts to eliminate references to single-tenant deployment, ensuring they only support multi-tenant configurations. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
-134
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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.
|
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)
|
- Build-Pipeline: [Dockerfile](Dockerfile)
|
||||||
- Release-Pipeline: [.github/workflows/release-docker.yml](.github/workflows/release-docker.yml)
|
- Release-Pipeline: [.github/workflows/release-docker.yml](.github/workflows/release-docker.yml)
|
||||||
- Frontend/Reverse Proxy: Nginx (Container)
|
- Frontend/Reverse Proxy: Nginx (Container)
|
||||||
@@ -237,7 +237,7 @@ docker compose logs -f nginx
|
|||||||
|
|
||||||
### Persistente Daten
|
### 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
|
- MongoDB Daten
|
||||||
- Uploads / Thumbnails / Previews / QRCodes
|
- Uploads / Thumbnails / Previews / QRCodes
|
||||||
|
|||||||
@@ -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())
|
||||||
-144
@@ -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 "$@"
|
|
||||||
@@ -1,536 +1,161 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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 <dir> Destination base directory (default: /var/backups)
|
|
||||||
# --db|--db-name <name> MongoDB database name (default: Inventarsystem)
|
|
||||||
# --uri|--mongo-uri <uri> MongoDB URI (default: mongodb://localhost:27017/)
|
|
||||||
# --invoice-archive-dir <dir> Invoice archive directory (default: <dest>/invoice-archive)
|
|
||||||
# --invoice-keep-days <N> Retention for invoice archive in days (default: 3650)
|
|
||||||
# --log <file> 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 <N> Age-based retention in days (default: 7; 0 disables age filter)
|
|
||||||
# --min-keep <N> Always keep at least N most recent backups (default: 7)
|
|
||||||
# --mode <auto|host|docker> Backup mode (default: auto)
|
|
||||||
# -h|--help Show help
|
|
||||||
# ========================================================
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_DIR="${PROJECT_DIR:-$SCRIPT_DIR}"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
# Default destination now /var/backups; override with --dest or BACKUP_BASE_DIR env
|
INVOICE_ARCHIVE_DIR="${INVOICE_ARCHIVE_DIR:-/var/backups/invoice-archive}"
|
||||||
BACKUP_BASE_DIR="${BACKUP_BASE_DIR:-/var/backups}"
|
KEEP_DAYS="${INVOICE_KEEP_DAYS:-3650}"
|
||||||
LOG_DIR="$PROJECT_DIR/logs"
|
MODE="auto"
|
||||||
# Create only the log directory here; backup dir is created later with sudo if needed
|
BASE_NAME=""
|
||||||
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
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $(basename "$0") [options]
|
Usage: $0 [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dest|--out|--backup-dir <dir> Destination base directory (default: $BACKUP_BASE_DIR)
|
--invoice-archive-dir DIR Directory for invoice archive files
|
||||||
--db|--db-name <name> MongoDB database name (default: $DB_NAME)
|
(default: $INVOICE_ARCHIVE_DIR)
|
||||||
--uri|--mongo-uri <uri> MongoDB URI (default: $MONGO_URI)
|
--invoice-keep-days N Remove archived invoice files older than N days
|
||||||
--invoice-archive-dir <dir> Invoice archive directory (default: $INVOICE_ARCHIVE_DIR)
|
(default: $KEEP_DAYS)
|
||||||
--invoice-keep-days <N> Retention for invoice archive in days (default: $INVOICE_KEEP_DAYS)
|
--mode auto|docker|host Backup mode (default: auto)
|
||||||
--log <file> Log file (default: $LOG_FILE)
|
--base-name NAME Base filename prefix for archive files
|
||||||
--no-compress Disable compression (keeps directory instead of .tar.gz)
|
-h, --help Show this help message
|
||||||
--compress-level <0-9> Compression level flag (only 0 is special here)
|
|
||||||
--keep-days <N> Age-based retention in days (default: $KEEP_DAYS; 0 disables age filter)
|
|
||||||
--min-keep <N> Always keep at least this many backups (default: $MIN_KEEP)
|
|
||||||
--mode <auto|host|docker> Backup mode (default: $BACKUP_MODE)
|
|
||||||
--multitenant Use docker-compose-multitenant.yml (default)
|
|
||||||
--singletenant Use docker-compose.yml
|
|
||||||
-h|--help Show this help and exit
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse CLI arguments
|
parse_args() {
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
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)
|
||||||
INVOICE_ARCHIVE_DIR="$2"; shift 2;;
|
INVOICE_ARCHIVE_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--invoice-keep-days)
|
--invoice-keep-days)
|
||||||
INVOICE_KEEP_DAYS="$2"; shift 2;;
|
KEEP_DAYS="$2"
|
||||||
--log)
|
shift 2
|
||||||
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)
|
--mode)
|
||||||
BACKUP_MODE="$2"; shift 2;;
|
MODE="$2"
|
||||||
--multitenant)
|
shift 2
|
||||||
COMPOSE_FILE="docker-compose-multitenant.yml"; shift;;
|
;;
|
||||||
--singletenant)
|
--base-name)
|
||||||
COMPOSE_FILE="docker-compose.yml"; shift;;
|
BASE_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage; exit 0;;
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1" >&2; usage; exit 2;;
|
echo "Error: unknown option '$1'" >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init_docker_compose() {
|
ensure_directory() {
|
||||||
if docker compose version >/dev/null 2>&1; then
|
mkdir -p "$INVOICE_ARCHIVE_DIR"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compose_cmd() {
|
cleanup_old_archives() {
|
||||||
if [ "$DOCKER_AVAILABLE" -ne 1 ]; then
|
if [[ -n "$KEEP_DAYS" ]]; then
|
||||||
return 1
|
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
|
fi
|
||||||
"${DOCKER_COMPOSE_CMD[@]}" "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try_backup_db_docker() {
|
host_backup() {
|
||||||
local out_dir="$1"
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
local stamp
|
return 1
|
||||||
stamp="$(date +"%Y-%m-%d_%H-%M-%S")"
|
fi
|
||||||
local archive_path="$out_dir/mongodb-${stamp}.archive.gz"
|
if ! python3 -c 'import pymongo' >/dev/null 2>&1; then
|
||||||
|
|
||||||
[ "$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
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if compose_cmd exec -T mongodb sh -c "mongodump --archive --gzip --db '$DB_NAME'" > "$archive_path"; then
|
python3 "$SCRIPT_DIR/Web/backup_invoices.py" \
|
||||||
log_message "Docker DB backup successful: $archive_path"
|
--archive-dir "$INVOICE_ARCHIVE_DIR" \
|
||||||
return 0
|
--base-name "$BASE_NAME"
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try_backup_invoices_docker() {
|
docker_backup() {
|
||||||
local archive_base="$1"
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
[ "$DOCKER_AVAILABLE" -eq 1 ] || return 1
|
return 1
|
||||||
|
fi
|
||||||
local tmp_base
|
if ! docker compose -f "$COMPOSE_FILE" ps -q app >/dev/null 2>&1; then
|
||||||
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
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local app_cid
|
local app_container
|
||||||
app_cid="$(compose_cmd ps -q app)"
|
app_container="$(docker compose -f "$COMPOSE_FILE" ps -q app | tr -d '[:space:]')"
|
||||||
[ -n "$app_cid" ] || return 1
|
if [[ -z "$app_container" ]]; then
|
||||||
|
|
||||||
if ! sudo docker cp "$PROJECT_DIR/Backup-Invoices.py" "$app_cid:/tmp/Backup-Invoices.py" >/dev/null 2>&1; then
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
local timestamp
|
||||||
timestamp="$(date +"%Y-%m-%d_%H-%M-%S")"
|
timestamp="$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||||
local archive_base
|
local output_dir
|
||||||
archive_base="$INVOICE_ARCHIVE_DIR/invoices-$timestamp"
|
local remote_base
|
||||||
|
|
||||||
local py_exec
|
output_dir="/tmp/invoice_archive_${timestamp}"
|
||||||
if [ -x "$PROJECT_DIR/.venv/bin/python" ]; then
|
remote_base="${output_dir}/${BASE_NAME}"
|
||||||
py_exec="$PROJECT_DIR/.venv/bin/python"
|
|
||||||
else
|
|
||||||
py_exec="python3"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local host_invoice_ok=0
|
docker compose -f "$COMPOSE_FILE" exec -T app mkdir -p "$output_dir"
|
||||||
local prefer_docker_invoice=0
|
docker compose -f "$COMPOSE_FILE" exec -T app python3 /app/Web/backup_invoices.py \
|
||||||
if [ "$BACKUP_MODE" = "auto" ] && [ "$DOCKER_AVAILABLE" -eq 1 ] && [ "$MONGO_URI" = "mongodb://localhost:27017/" ]; then
|
--archive-dir "$output_dir" \
|
||||||
prefer_docker_invoice=1
|
--base-name "$BASE_NAME"
|
||||||
log_message "Auto mode: preferring Docker invoice archive export"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$BACKUP_MODE" != "docker" ] && [ "$prefer_docker_invoice" -ne 1 ]; then
|
mkdir -p "$INVOICE_ARCHIVE_DIR"
|
||||||
if [ "${USE_NULL_OUTPUT:-false}" = true ]; then
|
docker cp "$app_container":"${remote_base}.jsonl" "$INVOICE_ARCHIVE_DIR/"
|
||||||
if sudo -E "$py_exec" "$PROJECT_DIR/Backup-Invoices.py" --uri "$MONGO_URI" --db "$DB_NAME" --out "$archive_base" > /dev/null 2>&1; then
|
docker cp "$app_container":"${remote_base}.csv" "$INVOICE_ARCHIVE_DIR/"
|
||||||
host_invoice_ok=1
|
docker cp "$app_container":"${remote_base}.meta.json" "$INVOICE_ARCHIVE_DIR/"
|
||||||
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
|
|
||||||
|
|
||||||
if [ "$host_invoice_ok" -ne 1 ]; then
|
docker compose -f "$COMPOSE_FILE" exec -T app rm -rf "$output_dir"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to create backup
|
main() {
|
||||||
create_backup() {
|
if [[ -z "$BASE_NAME" ]]; then
|
||||||
log_message "Starting backup process..."
|
BASE_NAME="invoices-$(date +'%Y-%m-%d_%H-%M-%S')"
|
||||||
|
|
||||||
# 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
|
fi
|
||||||
|
|
||||||
# Remove existing backup with same date if it exists
|
ensure_directory
|
||||||
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
|
case "$MODE" in
|
||||||
log_message "Removing existing backup archive at $BACKUP_ARCHIVE"
|
auto)
|
||||||
sudo rm -f "$BACKUP_ARCHIVE"
|
if docker_backup; then
|
||||||
fi
|
cleanup_old_archives
|
||||||
|
|
||||||
# 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"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
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
|
|
||||||
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"
|
|
||||||
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
|
|
||||||
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++))
|
|
||||||
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
|
return 0
|
||||||
|
fi
|
||||||
|
if host_backup; then
|
||||||
|
cleanup_old_archives
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Error: could not perform invoice backup in auto mode. Docker or host Python with pymongo is required." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
docker)
|
||||||
|
if docker_backup; then
|
||||||
|
cleanup_old_archives
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "Error: docker backup failed or app container is unavailable." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
host)
|
||||||
|
if host_backup; then
|
||||||
|
cleanup_old_archives
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
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
|
parse_args "$@"
|
||||||
log_message "Backup destination: $BACKUP_BASE_DIR"
|
main
|
||||||
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
|
|
||||||
|
|||||||
-218
@@ -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 <<EOF
|
|
||||||
Usage: $0 [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--remove-cron Remove matching cron entries for old systems
|
|
||||||
--keep-autostart Stop services but do not disable autostart
|
|
||||||
--dry-run Show actions without executing
|
|
||||||
-h, --help Show this help message
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "[cleanup-old] $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_cmd() {
|
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
|
||||||
echo "[dry-run] $*"
|
|
||||||
else
|
|
||||||
"$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_cmd_sudo() {
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
run_cmd "$@"
|
|
||||||
else
|
|
||||||
run_cmd sudo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_args() {
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--remove-cron)
|
|
||||||
REMOVE_CRON=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--keep-autostart)
|
|
||||||
KEEP_AUTOSTART=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Error: unknown option '$1'"
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
service_exists() {
|
|
||||||
local service="$1"
|
|
||||||
systemctl list-unit-files --type=service --no-legend --no-pager 2>/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 "$@"
|
|
||||||
@@ -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()
|
|
||||||
@@ -99,6 +99,7 @@ services:
|
|||||||
INVENTAR_REDIS_HOST: redis
|
INVENTAR_REDIS_HOST: redis
|
||||||
INVENTAR_REDIS_PORT: "6379"
|
INVENTAR_REDIS_PORT: "6379"
|
||||||
INVENTAR_REDIS_DB: "0"
|
INVENTAR_REDIS_DB: "0"
|
||||||
|
INVENTAR_TENANT_PORT_MAP: "${INVENTAR_TENANT_PORT_MAP:-}"
|
||||||
|
|
||||||
# Storage Configuration
|
# Storage Configuration
|
||||||
INVENTAR_BACKUP_FOLDER: /data/backups
|
INVENTAR_BACKUP_FOLDER: /data/backups
|
||||||
|
|||||||
-1104
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
@@ -176,7 +176,6 @@ Options:
|
|||||||
--skip-cleanup-old Do not run cleanup-old.sh after install
|
--skip-cleanup-old Do not run cleanup-old.sh after install
|
||||||
--cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup
|
--cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup
|
||||||
--multitenant Use docker-compose-multitenant.yml (default)
|
--multitenant Use docker-compose-multitenant.yml (default)
|
||||||
--singletenant Use docker-compose.yml
|
|
||||||
--legacy-db-name <name> Legacy database name (default: $LEGACY_DB_NAME)
|
--legacy-db-name <name> Legacy database name (default: $LEGACY_DB_NAME)
|
||||||
--legacy-mongo-uri <uri> Legacy Mongo URI (default: $LEGACY_MONGO_URI)
|
--legacy-mongo-uri <uri> Legacy Mongo URI (default: $LEGACY_MONGO_URI)
|
||||||
--legacy-system-dir <path> Optional old system directory to remove after migration
|
--legacy-system-dir <path> Optional old system directory to remove after migration
|
||||||
@@ -207,10 +206,6 @@ parse_args() {
|
|||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--singletenant)
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--legacy-db-name)
|
--legacy-db-name)
|
||||||
LEGACY_DB_NAME="$2"
|
LEGACY_DB_NAME="$2"
|
||||||
shift 2
|
shift 2
|
||||||
|
|||||||
@@ -124,6 +124,68 @@ EOF
|
|||||||
fi
|
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" <<EOF
|
||||||
|
NUITKA_BUILD=0
|
||||||
|
INVENTAR_TENANT_PORT_MAP=$tenant_port_map
|
||||||
|
EOF
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^INVENTAR_TENANT_PORT_MAP=' "$env_file" 2>/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() {
|
remove_tenant_port() {
|
||||||
local tenant_id="$1"
|
local tenant_id="$1"
|
||||||
local config_path="$CONFIG_FILE"
|
local config_path="$CONFIG_FILE"
|
||||||
@@ -239,6 +301,10 @@ case "$COMMAND" in
|
|||||||
fi
|
fi
|
||||||
register_tenant_port "$TENANT_ID" "$PORT_ARG"
|
register_tenant_port "$TENANT_ID" "$PORT_ARG"
|
||||||
update_runtime_ports "$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
|
fi
|
||||||
|
|
||||||
echo "Adding new tenant '$TENANT_ID'..."
|
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 port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||||
if [ -n "$port_to_remove" ]; then
|
if [ -n "$port_to_remove" ]; then
|
||||||
remove_runtime_port "$port_to_remove"
|
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."
|
echo "Removed tenant '$TENANT_ID' from config.json and cleaned runtime port $port_to_remove."
|
||||||
else
|
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."
|
echo "Removed tenant '$TENANT_ID' from config.json. No port mapping was present."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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 <<USAGE
|
|
||||||
Usage:
|
|
||||||
$0 pin <ref> [--restart] [--force] Persistently lock to a commit/tag/branch and deploy it
|
|
||||||
$0 use <ref> [--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:
|
|
||||||
- <ref> 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 "$@"
|
|
||||||
@@ -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 "$@"
|
|
||||||
-119
@@ -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
|
|
||||||
+6
-8
@@ -46,14 +46,16 @@ services:
|
|||||||
image: $app_image
|
image: $app_image
|
||||||
build: null
|
build: null
|
||||||
EOF
|
EOF
|
||||||
if [ ${#ports[@]} -gt 1 ]; then
|
if [ ${#ports[@]} -gt 0 ]; then
|
||||||
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
||||||
ports:
|
ports:
|
||||||
EOF
|
EOF
|
||||||
for port in "${ports[@]}"; do
|
for port in "${ports[@]}"; do
|
||||||
|
if [ -n "$port" ]; then
|
||||||
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
||||||
- "$port:8000"
|
- "$port:8000"
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -67,10 +69,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--singletenant)
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
@@ -93,13 +91,13 @@ if [ -f "$ENV_FILE" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$SCRIPT_DIR/Dockerfile" ]; then
|
if [ -f "$SCRIPT_DIR/Dockerfile" ]; then
|
||||||
docker compose "${compose_args[@]}" up -d --build app
|
docker compose "${compose_args[@]}" up -d --build --force-recreate app
|
||||||
else
|
else
|
||||||
echo "Warning: Dockerfile not found in $SCRIPT_DIR. Skipping build and restarting existing app container."
|
echo "Warning: Dockerfile not found in $SCRIPT_DIR. Skipping build and restarting existing app container."
|
||||||
if ! docker compose "${compose_args[@]}" up -d --no-build app; then
|
if ! docker compose "${compose_args[@]}" up -d --no-build --force-recreate app; then
|
||||||
echo "Warning: app image not found or not available locally. Attempting docker compose pull app..."
|
echo "Warning: app image not found or not available locally. Attempting docker compose pull app..."
|
||||||
docker compose "${compose_args[@]}" pull app
|
docker compose "${compose_args[@]}" pull app
|
||||||
docker compose "${compose_args[@]}" up -d --no-build app
|
docker compose "${compose_args[@]}" up -d --no-build --force-recreate app
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
-538
@@ -1,538 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
LOG_DIR="$SCRIPT_DIR/logs"
|
|
||||||
LOG_FILE="$LOG_DIR/restore.log"
|
|
||||||
WORK_DIR="$(mktemp -d /tmp/inventarsystem-restore-XXXXXX)"
|
|
||||||
|
|
||||||
DB_NAME="${INVENTAR_MONGODB_DB:-inventar_default}"
|
|
||||||
SOURCE_PATH=""
|
|
||||||
BACKUP_DATE=""
|
|
||||||
DROP_DATABASE=false
|
|
||||||
RESTART_SERVICES=false
|
|
||||||
STAGED_PATH=""
|
|
||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
|
||||||
|
|
||||||
BACKUP_ROOT_LOCAL="$SCRIPT_DIR/backups"
|
|
||||||
BACKUP_ROOT_SYSTEM="/var/backups"
|
|
||||||
|
|
||||||
SUDO=""
|
|
||||||
if [ "$(id -u)" -ne 0 ] && command -v sudo >/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 <<EOF
|
|
||||||
Inventarsystem Restore / Porting Tool (Docker-first)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
$0 --source <path> [options]
|
|
||||||
$0 --date <YYYY-MM-DD|latest> [options]
|
|
||||||
$0 --list
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--source <path> 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 <value> 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 <path> or --date <YYYY-MM-DD|latest>"
|
|
||||||
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 "$@"
|
|
||||||
Executable → Regular
+4
-47
@@ -1,47 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Helper script to run the backup with proper Python environment
|
set -euo pipefail
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# Get the script directory
|
exec "$SCRIPT_DIR/backup.sh" "$@"
|
||||||
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
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ Options:
|
|||||||
--no-cron Do not create or update cron jobs
|
--no-cron Do not create or update cron jobs
|
||||||
--with-cron Create/update cron jobs (default)
|
--with-cron Create/update cron jobs (default)
|
||||||
--multitenant Use the multi-tenant architecture deployment
|
--multitenant Use the multi-tenant architecture deployment
|
||||||
--singletenant Use the legacy single-tenant compose deployment
|
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -57,10 +56,6 @@ parse_args() {
|
|||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--singletenant)
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -584,15 +579,19 @@ EOF
|
|||||||
|
|
||||||
if [ -n "$HTTP_PORTS_VALUE" ]; then
|
if [ -n "$HTTP_PORTS_VALUE" ]; then
|
||||||
local ports_array
|
local ports_array
|
||||||
read -r -a ports_array <<<"$HTTP_PORTS_VALUE"
|
local ports_list
|
||||||
if [ "${#ports_array[@]}" -gt 1 ]; then
|
ports_list="${HTTP_PORTS_VALUE//,/ }"
|
||||||
|
read -r -a ports_array <<<"$ports_list"
|
||||||
|
if [ "${#ports_array[@]}" -gt 0 ]; then
|
||||||
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
||||||
ports:
|
ports:
|
||||||
EOF
|
EOF
|
||||||
for port in "${ports_array[@]}"; do
|
for port in "${ports_array[@]}"; do
|
||||||
|
if [ -n "$port" ]; then
|
||||||
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
cat >> "$RUNTIME_COMPOSE_OVERRIDE_FILE" <<EOF
|
||||||
- "$port:8000"
|
- "$port:8000"
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--singletenant)
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ Usage: $0 [options]
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--multitenant Use docker-compose-multitenant.yml (default)
|
--multitenant Use docker-compose-multitenant.yml (default)
|
||||||
--singletenant Use docker-compose.yml
|
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -164,10 +163,6 @@ parse_args() {
|
|||||||
COMPOSE_FILE="docker-compose-multitenant.yml"
|
COMPOSE_FILE="docker-compose-multitenant.yml"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--singletenant)
|
|
||||||
COMPOSE_FILE="docker-compose.yml"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user