Backup statuses

Default backups
This commit is contained in:
amcmanu3 2024-05-26 13:45:13 -04:00
parent bf196b68c0
commit 1b073a2401
8 changed files with 170 additions and 51 deletions

View File

@ -10,7 +10,6 @@ from peewee import (
TextField, TextField,
AutoField, AutoField,
BooleanField, BooleanField,
UUIDField,
) )
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
@ -18,6 +17,7 @@ from app.classes.models.base_model import BaseModel
from app.classes.models.users import HelperUsers from app.classes.models.users import HelperUsers
from app.classes.models.servers import Servers from app.classes.models.servers import Servers
from app.classes.models.server_permissions import PermissionsServers from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.helpers import Helpers
from app.classes.shared.websocket_manager import WebSocketManager from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -105,7 +105,7 @@ class Schedules(BaseModel):
# Backups Class # Backups Class
# ********************************************************************************** # **********************************************************************************
class Backups(BaseModel): class Backups(BaseModel):
backup_id = UUIDField(primary_key=True, default=uuid.uuid4) backup_id = CharField(primary_key=True, default=Helpers.create_uuid())
backup_name = CharField(default="New Backup") backup_name = CharField(default="New Backup")
backup_location = CharField(default="") backup_location = CharField(default="")
excluded_dirs = CharField(null=True) excluded_dirs = CharField(null=True)
@ -115,6 +115,8 @@ class Backups(BaseModel):
shutdown = BooleanField(default=False) shutdown = BooleanField(default=False)
before = CharField(default="") before = CharField(default="")
after = CharField(default="") after = CharField(default="")
default = BooleanField(default=False)
status = CharField(default='{"status": "Standby", "message": ""}')
enabled = BooleanField(default=True) enabled = BooleanField(default=True)
class Meta: class Meta:
@ -348,7 +350,7 @@ class HelpersManagement:
return model_to_dict(Backups.get(Backups.backup_id == backup_id)) return model_to_dict(Backups.get(Backups.backup_id == backup_id))
@staticmethod @staticmethod
def get_backups_by_server(server_id, model): def get_backups_by_server(server_id, model=False):
if not model: if not model:
data = {} data = {}
for backup in ( for backup in (
@ -365,11 +367,26 @@ class HelpersManagement:
"shutdown": backup.shutdown, "shutdown": backup.shutdown,
"before": backup.before, "before": backup.before,
"after": backup.after, "after": backup.after,
"default": backup.default,
"enabled": backup.enabled,
} }
else: else:
data = Backups.select().where(Backups.server_id == server_id).execute() data = Backups.select().where(Backups.server_id == server_id).execute()
return data return data
@staticmethod
def get_default_server_backup(server_id: str) -> dict:
bu_query = Backups.select().where(
Backups.server_id == server_id & Backups.default == True
)
backup_model = bu_query.first()
if backup_model:
return model_to_dict(backup_model)
else:
raise IndexError
@staticmethod @staticmethod
def remove_backup_config(backup_id): def remove_backup_config(backup_id):
Backups.delete().where(Backups.backup_id == backup_id).execute() Backups.delete().where(Backups.backup_id == backup_id).execute()

View File

@ -1107,12 +1107,12 @@ class ServerInstance:
f.write("eula=true") f.write("eula=true")
self.run_threaded_server(user_id) self.run_threaded_server(user_id)
def a_backup_server(self, backup_id): def server_backup_threader(self, backup_id, update=False):
backup_thread = threading.Thread( backup_thread = threading.Thread(
target=self.backup_server, target=self.backup_server,
daemon=True, daemon=True,
name=f"backup_{self.name}", name=f"backup_{self.name}",
args=[backup_id], args=[backup_id, update],
) )
logger.info( logger.info(
f"Starting Backup Thread for server {self.settings['server_name']}." f"Starting Backup Thread for server {self.settings['server_name']}."
@ -1140,7 +1140,7 @@ class ServerInstance:
logger.info(f"Backup Thread started for server {self.settings['server_name']}.") logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
@callback @callback
def backup_server(self, backup_id): def backup_server(self, backup_id, update):
was_server_running = None was_server_running = None
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup") logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id) server_users = PermissionsServers.get_server_user_list(self.server_id)
@ -1174,9 +1174,12 @@ class ServerInstance:
"Found shutdown preference. Delaying" "Found shutdown preference. Delaying"
+ "backup start. Shutting down server." + "backup start. Shutting down server."
) )
if self.check_running(): if not update:
self.stop_server() if self.check_running():
was_server_running = True self.stop_server()
was_server_running = True
else:
was_server_running = False
self.helper.ensure_dir_exists(backup_location) self.helper.ensure_dir_exists(backup_location)
try: try:
@ -1318,7 +1321,7 @@ class ServerInstance:
def jar_update(self): def jar_update(self):
self.stats_helper.set_update(True) self.stats_helper.set_update(True)
update_thread = threading.Thread( update_thread = threading.Thread(
target=self.a_jar_update, daemon=True, name=f"exe_update_{self.name}" target=self.threaded_jar_update, daemon=True, name=f"exe_update_{self.name}"
) )
update_thread.start() update_thread.start()
@ -1359,10 +1362,13 @@ class ServerInstance:
def check_update(self): def check_update(self):
return self.stats_helper.get_server_stats()["updating"] return self.stats_helper.get_server_stats()["updating"]
def a_jar_update(self): def threaded_jar_update(self):
server_users = PermissionsServers.get_server_user_list(self.server_id) server_users = PermissionsServers.get_server_user_list(self.server_id)
was_started = "-1" was_started = "-1"
self.a_backup_server() # Get default backup configuration
backup_config = HelpersManagement.get_default_server_backup(self.server_id)
# start threaded backup
self.server_backup_threader(backup_config["backup_id"], True)
# checks if server is running. Calls shutdown if it is running. # checks if server is running. Calls shutdown if it is running.
if self.check_running(): if self.check_running():
was_started = True was_started = True
@ -1516,12 +1522,6 @@ class ServerInstance:
WebSocketManager().broadcast_user_page( WebSocketManager().broadcast_user_page(
user, "/panel/dashboard", "send_start_reload", {} user, "/panel/dashboard", "send_start_reload", {}
) )
WebSocketManager().broadcast_user(
user,
"notification",
"Executable update finished for " + self.name,
)
self.management_helper.add_to_audit_log_raw( self.management_helper.add_to_audit_log_raw(
"Alert", "Alert",
"-1", "-1",

View File

@ -140,7 +140,7 @@ class TasksManager:
) )
elif command == "backup_server": elif command == "backup_server":
svr.a_backup_server(cmd["action_id"]) svr.server_backup_threader(cmd["action_id"])
elif command == "update_executable": elif command == "update_executable":
svr.jar_update() svr.jar_update()

View File

@ -124,6 +124,15 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
server_id, server_id,
self.get_remote_ip(), self.get_remote_ip(),
) )
if backup_conf["default"]:
return self.finish_json(
405,
{
"status": "error",
"error": "NOT_ALLOWED",
"error_data": "Cannot delete default backup",
},
)
self.controller.management.delete_backup_config(backup_id) self.controller.management.delete_backup_config(backup_id)
return self.finish_json(200, {"status": "ok"}) return self.finish_json(200, {"status": "ok"})

View File

@ -70,7 +70,9 @@
<tr class="rounded"> <tr class="rounded">
<th scope="col" style="width: 15%; min-width: 10px;">{{ translate('serverBackups', 'name', <th scope="col" style="width: 15%; min-width: 10px;">{{ translate('serverBackups', 'name',
data['lang']) }} </th> data['lang']) }} </th>
<th scope="col" style="width: 60%; min-width: 50px;">{{ translate('serverBackups', <th scope="col" style="width: 5%; min-width: 10px;">{{ translate('serverBackups', 'status',
data['lang']) }} </th>
<th scope="col" style="width: 55%; min-width: 50px;">{{ translate('serverBackups',
'storageLocation', data['lang']) }}</th> 'storageLocation', data['lang']) }}</th>
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups', <th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups',
'maxBackups', data['lang']) }}</th> 'maxBackups', data['lang']) }}</th>
@ -83,6 +85,19 @@
<tr> <tr>
<td id="{{backup.backup_name}}" class="id"> <td id="{{backup.backup_name}}" class="id">
<p>{{backup.backup_name}}</p> <p>{{backup.backup_name}}</p>
<br>
{% if backup.default %}
<span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</td>
<td>
<div id="{{backup.backup_id}}_status">
<span class="badge-pill badge-outline-success backup-status"
data-status="{{ backup.status }}"></span>
</div>
</td> </td>
<td id="{{backup.backup_location}}" class="type"> <td id="{{backup.backup_location}}" class="type">
<p>{{backup.backup_location}}</p> <p>{{backup.backup_location}}</p>
@ -96,9 +111,11 @@
class="btn btn-info"> class="btn btn-info">
<i class="fas fa-pencil-alt"></i> <i class="fas fa-pencil-alt"></i>
</button> </button>
{% if not backup.default %}
<button data-backup={{ backup.backup_id }} class="btn btn-danger del_button"> <button data-backup={{ backup.backup_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
{% end %}
<button data-backup={{ backup.backup_id }} data-toggle="tooltip" <button data-backup={{ backup.backup_id }} data-toggle="tooltip"
title="{{ translate('serverBackups', 'run', data['lang']) }}" title="{{ translate('serverBackups', 'run', data['lang']) }}"
class="btn btn-outline-warning run-backup backup_now_button"> class="btn btn-outline-warning run-backup backup_now_button">
@ -126,6 +143,13 @@
<tr> <tr>
<td id="{{backup.backup_name}}" class="id"> <td id="{{backup.backup_name}}" class="id">
<p>{{backup.backup_name}}</p> <p>{{backup.backup_name}}</p>
<br>
{% if backup.default %}
<span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</td> </td>
<td id="backup_edit" class="action"> <td id="backup_edit" class="action">
<button <button
@ -133,9 +157,11 @@
class="btn btn-info"> class="btn btn-info">
<i class="fas fa-pencil-alt"></i> <i class="fas fa-pencil-alt"></i>
</button> </button>
{% if not backup.default %}
<button data-backup={{ backup.backup_id }} class="btn btn-danger del_button"> <button data-backup={{ backup.backup_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
{% end %}
<button data-backup={{ backup.backup_id }} data-toggle="tooltip" <button data-backup={{ backup.backup_id }} data-toggle="tooltip"
title="{{ translate('serverBackups', 'run', data['lang']) }}" title="{{ translate('serverBackups', 'run', data['lang']) }}"
class="btn btn-outline-warning test-socket"> class="btn btn-outline-warning test-socket">
@ -230,14 +256,7 @@
let responseData = await res.json(); let responseData = await res.json();
if (responseData.status === "ok") { if (responseData.status === "ok") {
console.log(responseData); console.log(responseData);
$("#backup_button").html(`<div class="progress" style="height: 15px;"> $("#backup_button").prop('disabled', true)
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>`);
} else { } else {
bootbox.alert({ bootbox.alert({
@ -269,6 +288,54 @@
$(document).ready(function () { $(document).ready(function () {
console.log("ready!"); console.log("ready!");
$(".backup-explain").on("click", function () {
bootbox.alert($(this).data("explain"));
});
$('.backup-status').each(function () {
// Get the JSON string from the element's text
var data = $(this).data('status');
try {
console.log("DATA " + data)
// Update the element's text with the status value
$(this).text(data.status);
// Optionally, add classes based on status to style the element
if (data.status === 'Active') {
$(this).addClass('badge-pill badge-outline-success');
} else if (data.status === 'Failed') {
$(this).addClass('badge-pill badge-outline-danger');
} else if (data.status === 'Standby') {
$(this).addClass('badge-pill badge-outline-secondary');
}
} catch (e) {
console.error('Invalid JSON string:', jsonString);
}
});
if (webSocket) {
webSocket.on('backup_status', function (backup) {
console.log("PEEPEEPOOPOO")
text = ``;
console.log(backup)
if (backup.percent >= 100) {
$(`#${backup.backup_id}_status`).html(`<span class="badge-pill badge-outline-success backup-status"
>Completed</span>`);
setTimeout(function () {
window.location.reload(1);
}, 5000);
} else {
text = `<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width:${backup.percent}%;"
aria-valuenow="${backup.percent}" aria-valuemin="0" aria-valuemax="100">${backup.percent}%</div>`
$(`#${backup.backup_id}_status`).html(text);
}
});
}
$('#backup_table').DataTable({ $('#backup_table').DataTable({
"order": [[1, "desc"]], "order": [[1, "desc"]],
@ -378,21 +445,6 @@
bootbox.alert("You must input a path before selecting this button"); bootbox.alert("You must input a path before selecting this button");
} }
}); });
if (webSocket) {
webSocket.on('backup_status', function (backup) {
if (backup.percent >= 100) {
document.getElementById('backup_progress_bar').innerHTML = '100%';
document.getElementById('backup_progress_bar').style.width = '100%';
setTimeout(function () {
window.location.reload(1);
}, 5000);
} else {
document.getElementById('backup_progress_bar').innerHTML = backup.percent + '%';
document.getElementById('backup_progress_bar').style.width = backup.percent + '%';
document.getElementById('total_files').innerHTML = backup.total_files;
}
});
}
function getDirView(event) { function getDirView(event) {
let path = event.target.parentElement.getAttribute("data-path"); let path = event.target.parentElement.getAttribute("data-path");

View File

@ -63,7 +63,14 @@
{% end %} {% end %}
<form id="backup-form" class="forms-sample"> <form id="backup-form" class="forms-sample">
<div class="form-group"> <div class="form-group">
<label for="backup_name">{{ translate('serverBackups', 'name', data['lang']) }} </label> <label for="backup_name">{{ translate('serverBackups', 'name', data['lang']) }}
{% if data["backup_config"]["default"] %}
&nbsp;&nbsp; <span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</label>
{% if data["backup_config"].get("backup_id", None) %} {% if data["backup_config"].get("backup_id", None) %}
<input type="text" class="form-control" name="backup_name" id="backup_name" <input type="text" class="form-control" name="backup_name" id="backup_name"
value="{{ data['backup_config']['backup_name'] }}"> value="{{ data['backup_config']['backup_name'] }}">
@ -188,7 +195,8 @@
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) <button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang'])
}}</button> }}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang']) <button type="reset" class="btn btn-light cancel-button">{{ translate('serverBackups', 'cancel',
data['lang'])
}}</button> }}</button>
</form> </form>
</div> </div>
@ -431,6 +439,13 @@
} }
$(document).ready(function () { $(document).ready(function () {
$(".backup-explain").on("click", function (e) {
e.preventDefault();
bootbox.alert($(this).data("explain"));
});
$(".cancel-button").on("click", function () {
location.href = `/panel/server_detail?id=${server_id}&subpage=backup`
});
$("#backup-form").on("submit", async function (e) { $("#backup-form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
const token = getCookie("_xsrf") const token = getCookie("_xsrf")

View File

@ -3,9 +3,11 @@ import uuid
import peewee import peewee
import logging import logging
from app.classes.models.management import Backups, Schedules
from app.classes.shared.helpers import Helpers
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.migration import Migrator, MigrateHistory from app.classes.shared.migration import Migrator, MigrateHistory
from app.classes.models.management import Backups, Schedules
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,11 +17,20 @@ def migrate(migrator: Migrator, database, **kwargs):
Write your migrations here. Write your migrations here.
""" """
db = database db = database
Console.info("Starting Backups migrations")
Console.info(
"Migrations: Adding columns [backup_id, "
"backup_name, backup_location, enabled, default, action_id, backup_status]"
)
migrator.add_columns("backups", backup_id=peewee.UUIDField(default=uuid.uuid4)) migrator.add_columns("backups", backup_id=peewee.UUIDField(default=uuid.uuid4))
migrator.add_columns("backups", backup_name=peewee.CharField(default="Default")) migrator.add_columns("backups", backup_name=peewee.CharField(default="Default"))
migrator.add_columns("backups", backup_location=peewee.CharField(default="")) migrator.add_columns("backups", backup_location=peewee.CharField(default=""))
migrator.add_columns("backups", enabled=peewee.BooleanField(default=True)) migrator.add_columns("backups", enabled=peewee.BooleanField(default=True))
migrator.add_columns("backups", default=peewee.BooleanField(default=False))
migrator.add_columns(
"backups",
status=peewee.CharField(default='{"status": "Standby", "message": ""}'),
)
migrator.add_columns( migrator.add_columns(
"schedules", action_id=peewee.CharField(null=True, default=None) "schedules", action_id=peewee.CharField(null=True, default=None)
) )
@ -52,7 +63,7 @@ def migrate(migrator: Migrator, database, **kwargs):
database = db database = db
class NewBackups(peewee.Model): class NewBackups(peewee.Model):
backup_id = peewee.UUIDField(primary_key=True, default=uuid.uuid4) backup_id = peewee.CharField(primary_key=True, default=Helpers.create_uuid())
backup_name = peewee.CharField(default="New Backup") backup_name = peewee.CharField(default="New Backup")
backup_location = peewee.CharField(default="") backup_location = peewee.CharField(default="")
excluded_dirs = peewee.CharField(null=True) excluded_dirs = peewee.CharField(null=True)
@ -62,6 +73,8 @@ def migrate(migrator: Migrator, database, **kwargs):
shutdown = peewee.BooleanField(default=False) shutdown = peewee.BooleanField(default=False)
before = peewee.CharField(default="") before = peewee.CharField(default="")
after = peewee.CharField(default="") after = peewee.CharField(default="")
default = peewee.BooleanField(default=False)
status = peewee.CharField(default='{"status": "Standby", "message": ""}')
enabled = peewee.BooleanField(default=True) enabled = peewee.BooleanField(default=True)
class Meta: class Meta:
@ -97,11 +110,11 @@ def migrate(migrator: Migrator, database, **kwargs):
for backup in Backups.select(): for backup in Backups.select():
# Fetch the related server entry from the Servers table # Fetch the related server entry from the Servers table
server = Servers.get(Servers.server_id == backup.server_id) server = Servers.get(Servers.server_id == backup.server_id)
Console.info(f"Migrations: Migrating backup for server {server.server_name}")
# Create a new backup entry with data from the # Create a new backup entry with data from the
# old backup entry and related server # old backup entry and related server
NewBackups.create( NewBackups.create(
backup_name="Default", backup_name=f"{server.server_name} Backup",
# Set backup_location equal to backup_path # Set backup_location equal to backup_path
backup_location=server.backup_path, backup_location=server.backup_path,
excluded_dirs=backup.excluded_dirs, excluded_dirs=backup.excluded_dirs,
@ -111,19 +124,27 @@ def migrate(migrator: Migrator, database, **kwargs):
shutdown=backup.shutdown, shutdown=backup.shutdown,
before=backup.before, before=backup.before,
after=backup.after, after=backup.after,
default=True,
enabled=True, enabled=True,
) )
Console.debug("Migrations: Dropping old backup table")
# Drop the existing backups table # Drop the existing backups table
migrator.drop_table("backups") migrator.drop_table("backups")
Console.debug("Migrations: Renaming new_backups to backups")
# Rename the new table to backups # Rename the new table to backups
migrator.rename_table("new_backups", "backups") migrator.rename_table("new_backups", "backups")
Console.debug("Migrations: Dropping backup_path from servers table")
migrator.drop_columns("servers", ["backup_path"]) migrator.drop_columns("servers", ["backup_path"])
for schedule in Schedules.select(): for schedule in Schedules.select():
action_id = None action_id = None
if schedule.command == "backup_server": if schedule.command == "backup_server":
Console.info(
f"Migrations: Adding backup ID to task with name {schedule.name}"
)
backup = NewBackups.get(NewBackups.server_id == schedule.server_id) backup = NewBackups.get(NewBackups.server_id == schedule.server_id)
action_id = backup.backup_id action_id = backup.backup_id
NewSchedules.create( NewSchedules.create(
@ -144,9 +165,11 @@ def migrate(migrator: Migrator, database, **kwargs):
next_run=schedule.next_run, next_run=schedule.next_run,
) )
Console.debug("Migrations: dropping old schedules table")
# Drop the existing backups table # Drop the existing backups table
migrator.drop_table("schedules") migrator.drop_table("schedules")
Console.debug("Migrations: renaming new_schedules to schedules")
# Rename the new table to backups # Rename the new table to backups
migrator.rename_table("new_schedules", "schedules") migrator.rename_table("new_schedules", "schedules")

View File

@ -335,7 +335,10 @@
"newBackup": "Create New Backup", "newBackup": "Create New Backup",
"edit": "Edit", "edit": "Edit",
"run": "Run Backup", "run": "Run Backup",
"backups": "Server Backups" "backups": "Server Backups",
"default": "Default Backup",
"defaultExplain": "The backup that Crafty will use before updates. This cannot be changed or deleted.",
"status": "Status"
}, },
"serverConfig": { "serverConfig": {
"bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.", "bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.",