mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add backup UI page and update cache file via bwcli as well
This commit is contained in:
parent
9a8e0a38ff
commit
29ab3167e7
4 changed files with 133 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
18
src/common/core/backup/ui/actions.py
Normal file
18
src/common/core/backup/ui/actions.py
Normal 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
|
||||
87
src/common/core/backup/ui/template.html
Normal file
87
src/common/core/backup/ui/template.html
Normal 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 %}
|
||||
|
|
@ -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}")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue