Add backup UI page and update cache file via bwcli as well

This commit is contained in:
Théophile Diot 2024-04-02 21:45:02 +01:00
parent 9a8e0a38ff
commit 29ab3167e7
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
4 changed files with 133 additions and 14 deletions

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from datetime import datetime, timedelta
from json import dumps, loads
from os import getenv, sep
from os.path import join
from pathlib import Path
@ -30,9 +31,10 @@ try:
JOB = Job(LOGGER)
last_backup = JOB.get_cache("last_backup.txt")
if last_backup:
last_backup = datetime.fromisoformat(last_backup.decode())
last_backup = loads(JOB.get_cache("backup.json") or "{}")
last_backup_date = last_backup.get("date", None)
if last_backup_date:
last_backup_date = datetime.fromisoformat(last_backup_date)
current_time = datetime.now()
backup_period = getenv("BACKUP_SCHEDULE", "daily")
@ -42,7 +44,7 @@ try:
"monthly": timedelta(weeks=4).total_seconds(),
}
if last_backup and last_backup.timestamp() + PERIOD_STAMPS[backup_period] > current_time.timestamp():
if last_backup_date and last_backup_date.timestamp() + PERIOD_STAMPS[backup_period] > current_time.timestamp():
LOGGER.info(f"Backup already done within the last {backup_period} period, skipping backup ...")
sys_exit(0)
@ -73,9 +75,11 @@ try:
LOGGER.warning(f"Removing old backup file: {file}, as the rotation limit has been reached ...")
file.unlink()
cached, err = JOB.cache_file("last_backup.txt", current_time.isoformat().encode())
backup_files = sorted([file.name for file in backup_dir.glob("backup-*.zip")])
cached, err = JOB.cache_file("backup.json", dumps({"date": current_time.isoformat(), "files": backup_files}, indent=2).encode())
if not cached:
LOGGER.error(f"Failed to cache last_backup.txt :\n{err}")
LOGGER.error(f"Failed to cache backup.json :\n{err}")
status = 2
except SystemExit as e:
status = e.code

View file

@ -0,0 +1,18 @@
from datetime import datetime
from json import loads
def pre_render(app, *args, **kwargs):
try:
data = loads(app.config["DB"].get_job_cache_file("backup-data", "backup.json") or "{}")
if data.get("date", None):
data["date"] = datetime.fromisoformat(data["date"]).strftime("%Y-%m-%d %H:%M:%S")
return data
except:
return {"date": None, "files": []}
def backup(**kwargs):
pass

View file

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block content %}
<input type="csrf_token"
name="csrf_token"
value="{{ csrf_token }}"
class="hidden"
hidden />
<div class="core-layout">
{% if is_used %}
<div class="core-card">
<h5 class="core-card-title">INFO</h5>
<div class="core-card-text-container">
<p data-info class="core-card-text">{{ plugin.get("description") }}</p>
</div>
</div>
<!-- end info -->
<div class="core-layout-separator"></div>
{% if pre_render["status"] and pre_render["status"] == "ko" or "error" in pre_render["data"] %}
<div class="flex justify-center col-span-12">
<p class="text-white">Error during pre rendering</p>
<div class="ml-2">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 stroke-red-500 fill-white">
<path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</div>
</div>
{% endif %}
{% if pre_render["status"] and pre_render["status"] == "ok" and "error" not in pre_render["data"] %}
<div class="core-card">
<div class="core-card-wrap">
<h5 class="core-card-title">Last Backup</h5>
</div>
<div class="core-card-text-container">
<p data-info class="core-card-text">{{ pre_render["data"]["date"] }}</p>
</div>
</div>
<div class="core-card-list">
<div class="core-card-list-title-container">
<h5 class="core-card-list-title">Backup files</h5>
</div>
<div class="core-card-list-container">
<!-- list container-->
<div class="core-card-list-wrap">
<!-- list -->
<ul class="col-span-12 w-full">
{% for item in pre_render['data']["files"] %}
<li class="core-card-list-item">
<p class="core-card-list-item-content col-span-12">{{ item }}</p>
</li>
{% endfor %}
</ul>
<!-- end list-->
</div>
<!-- end list container-->
</div>
</div>
{% endif %}
{% else %}
<div class="core-card">
<div class="core-card-wrap">
<h5 class="core-card-title">Deactivated</h5>
<!-- icon -->
<div role="img" class="core-card-svg-container">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="core-card-deactivated-svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
</div>
<!-- end icon -->
</div>
<div class="core-card-text-container">
<p data-info class="core-card-text">This plugin need to be activated to access page.</p>
</div>
</div>
<!-- end info -->
{% endif %}
</div>
{% endblock %}

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from datetime import datetime
from json import dumps, loads
from os import environ, getenv
from os.path import join, sep
from pathlib import Path
@ -15,6 +16,7 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
if deps_path not in sys_path:
sys_path.append(deps_path)
from common_utils import bytes_hash # type: ignore
from Database import Database # type: ignore
from logger import setup_logger # type: ignore
from model import Base # type: ignore
@ -38,16 +40,16 @@ def acquire_db_lock():
def backup_database(current_time: datetime, backup_dir: Path = BACKUP_DIR):
"""Backup the database."""
database_uri = getenv("DATABASE_URI", "sqlite:////var/lib/bunkerweb/db.sqlite3")
db = Database(LOGGER)
database: Literal["sqlite", "mariadb", "mysql", "postgresql"] = database_uri.split(":")[0].split("+")[0] # type: ignore
database: Literal["sqlite", "mariadb", "mysql", "postgresql"] = db.database_uri.split(":")[0].split("+")[0] # type: ignore
backup_file = backup_dir.joinpath(f"backup-{database}-{current_time.strftime('%Y-%m-%d_%H-%M-%S')}.zip")
LOGGER.debug(f"Backup file path: {backup_file}")
if database == "sqlite":
match = DB_STRING_RX.search(database_uri)
match = DB_STRING_RX.search(db.database_uri)
if not match:
LOGGER.error(f"Invalid database string provided: {database_uri}, skipping backup ...")
LOGGER.error(f"Invalid database string provided: {db.database_uri}, skipping backup ...")
sys_exit(1)
db_path = Path(match.group("path"))
@ -56,16 +58,16 @@ def backup_database(current_time: datetime, backup_dir: Path = BACKUP_DIR):
proc = run(["sqlite3", db_path.as_posix(), ".dump"], stdout=PIPE, stderr=PIPE)
else:
db_host = database_uri.rsplit("@", 1)[1].split("/")[0].split(":")
db_host = db.database_uri.rsplit("@", 1)[1].split("/")[0].split(":")
db_port = None
if len(db_host) == 1:
db_host = db_host[0]
else:
db_host, db_port = db_host
db_user = database_uri.split("://")[1].split(":")[0]
db_password = database_uri.split("://")[1].split(":")[1].rsplit("@", 1)[0]
db_database_name = database_uri.split("/")[-1]
db_user = db.database_uri.split("://")[1].split(":")[0]
db_password = db.database_uri.split("://")[1].split(":")[1].rsplit("@", 1)[0]
db_database_name = db.database_uri.split("/")[-1]
if database in ("mariadb", "mysql"):
LOGGER.info("Creating a backup for the MariaDB/MySQL database ...")
@ -93,6 +95,14 @@ def backup_database(current_time: datetime, backup_dir: Path = BACKUP_DIR):
backup_file.chmod(0o600)
backup_data = loads(db.get_job_cache_file("backup-data", "backup.json") or "{}")
backup_data["files"] = sorted([file.name for file in backup_dir.glob("backup-*.zip")])
content = dumps(backup_data, indent=2).encode()
checksum = bytes_hash(content)
err = db.upsert_job_cache(None, "backup.json", content, job_name="backup-data", checksum=checksum)
if err:
LOGGER.error(f"Failed to update the backup.json cache file: {err}")
LOGGER.info(f"💾 Backup {backup_file.name} created successfully in {backup_dir}")