Backups are editable!

This commit is contained in:
amcmanu3 2024-05-25 16:33:28 -04:00
parent b061ebf5e5
commit 3cf4ebf073
8 changed files with 213 additions and 58 deletions

View File

@ -357,7 +357,7 @@ class HelpersManagement:
data[str(backup.backup_id)] = {
"backup_id": backup.backup_id,
"backup_name": backup.backup_name,
"backup_path": backup.backup_location,
"backup_location": backup.backup_location,
"excluded_dirs": backup.excluded_dirs,
"max_backups": backup.max_backups,
"server_id": backup.server_id_id,

View File

@ -235,6 +235,7 @@ class FileHelpers:
path_to_zip,
excluded_dirs,
server_id,
backup_id,
comment="",
compressed=None,
):
@ -306,6 +307,7 @@ class FileHelpers:
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
"backup_id": backup_id,
}
# send status results to page.
WebSocketManager().broadcast_page_params(

View File

@ -1197,6 +1197,7 @@ class ServerInstance:
server_dir,
excluded_dirs,
self.server_id,
backup_id,
conf["backup_name"],
conf["compress"],
)
@ -1205,7 +1206,7 @@ class ServerInstance:
len(self.list_backups(backup_location)) > conf["max_backups"]
and conf["max_backups"] > 0
):
backup_list = self.list_backups()
backup_list = self.list_backups(conf["backup_location"])
oldfile = backup_list[0]
oldfile_path = f"{backup_location}/{oldfile['path']}"
logger.info(f"Removing old backup '{oldfile['path']}'")
@ -1213,7 +1214,12 @@ class ServerInstance:
self.is_backingup = False
logger.info(f"Backup of server: {self.name} completed")
results = {"percent": 100, "total_files": 0, "current_file": 0}
results = {
"percent": 100,
"total_files": 0,
"current_file": 0,
"backup_id": backup_id,
}
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
@ -1251,7 +1257,12 @@ class ServerInstance:
logger.exception(
f"Failed to create backup of server {self.name} (ID {self.server_id})"
)
results = {"percent": 100, "total_files": 0, "current_file": 0}
results = {
"percent": 100,
"total_files": 0,
"current_file": 0,
"backup_id": backup_id,
}
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
@ -1267,17 +1278,6 @@ class ServerInstance:
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
self.last_backup_failed = True
def backup_status(self, source_path, dest_path):
results = Helpers.calc_percent(source_path, dest_path)
self.backup_stats = results
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"backup_status",
results,
)
def last_backup_status(self):
return self.last_backup_failed

View File

@ -1286,7 +1286,9 @@ class PanelHandler(BaseHandler):
)
self.controller.servers.refresh_server_settings(server_id)
try:
page_data["backup_list"] = server.list_backups()
page_data["backup_list"] = server.list_backups(
page_data["backup_config"]["backup_location"]
)
except:
page_data["backup_list"] = []
page_data["backup_path"] = Helpers.wtol_path(

View File

@ -218,7 +218,7 @@ def api_handlers(handler_args):
handler_args,
),
(
r"/api/v2/servers/([a-z0-9-]+)/backups/backup/?",
r"/api/v2/servers/([a-z0-9-]+)/backups/backup/([a-z0-9-]+)/?",
ApiServersServerBackupsBackupIndexHandler,
handler_args,
),

View File

@ -11,7 +11,7 @@ from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
backup_schema = {
BACKUP_SCHEMA = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
@ -19,11 +19,40 @@ backup_schema = {
"additionalProperties": False,
"minProperties": 1,
}
BACKUP_PATCH_SCHEMA = {
"type": "object",
"properties": {
"backup_location": {"type": "string", "minLength": 1},
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"before": {"type": "string"},
"after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
BASIC_BACKUP_PATCH_SCHEMA = {
"type": "object",
"properties": {
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"before": {"type": "string"},
"after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
def get(self, server_id: str):
def get(self, server_id: str, backup_id: str):
auth_data = self.authenticate_user()
backup_conf = self.controller.management.get_backup_config(backup_id)
if not auth_data:
return
mask = self.controller.server_perms.get_lowest_api_perm_mask(
@ -32,15 +61,40 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
),
auth_data[5],
)
if backup_conf["server_id"]["server_id"] != server_id:
return self.finish_json(
400,
{
"status": "error",
"error": "ID_MISMATCH",
"error_data": "Server ID backup server ID different",
},
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Authorization Error",
},
)
self.finish_json(200, backup_conf)
def delete(self, server_id: str):
def delete(self, server_id: str, backup_id: str):
auth_data = self.authenticate_user()
backup_conf = self.controller.management.get_backup_config(server_id)
backup_conf = self.controller.management.get_backup_config(backup_id)
if backup_conf["server_id"]["server_id"] != server_id:
return self.finish_json(
400,
{
"status": "error",
"error": "ID_MISMATCH",
"error_data": "Server ID backup server ID different",
},
)
if not auth_data:
return
mask = self.controller.server_perms.get_lowest_api_perm_mask(
@ -52,7 +106,14 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Authorization Error",
},
)
try:
data = json.loads(self.request.body)
@ -61,7 +122,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, backup_schema)
validate(data, BACKUP_SCHEMA)
except ValidationError as e:
return self.finish_json(
400,
@ -74,7 +135,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
try:
FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"])
os.path.join(backup_conf["backup_location"], data["filename"])
)
except Exception as e:
return self.finish_json(
@ -89,7 +150,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
return self.finish_json(200, {"status": "ok"})
def post(self, server_id: str):
def post(self, server_id: str, backup_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
@ -102,7 +163,24 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Authorization Error",
},
)
backup_config = self.controller.management.get_backup_config(backup_id)
if backup_config["server_id"] != server_id:
return self.finish_json(
400,
{
"status": "error",
"error": "ID_MISMATCH",
"error_data": "Server ID backup server ID different",
},
)
try:
data = json.loads(self.request.body)
@ -111,7 +189,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, backup_schema)
validate(data, BACKUP_SCHEMA)
except ValidationError as e:
return self.finish_json(
400,
@ -184,7 +262,6 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(server_id)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
@ -221,3 +298,70 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
)
return self.finish_json(200, {"status": "ok"})
def patch(self, server_id: str, backup_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
if auth_data[4]["superuser"]:
validate(data, BACKUP_PATCH_SCHEMA)
else:
validate(data, BASIC_BACKUP_PATCH_SCHEMA)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
backup_conf = self.controller.management.get_backup_config(backup_id)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Authorization Error",
},
)
if backup_conf["server_id"]["server_id"] != server_id:
return self.finish_json(
400,
{
"status": "error",
"error": "ID_MISMATCH",
"error_data": "Server ID backup server ID different",
},
)
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"error_data": "Authorization Error",
},
)
self.controller.management.update_backup_config(backup_id, data)
return self.finish_json(200, {"status": "ok"})

View File

@ -10,12 +10,12 @@ logger = logging.getLogger(__name__)
backup_patch_schema = {
"type": "object",
"properties": {
"backup_path": {"type": "string", "minLength": 1},
"backup_location": {"type": "string", "minLength": 1},
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"before": {"type": "string"},
"after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
@ -28,8 +28,8 @@ basic_backup_patch_schema = {
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"before": {"type": "string"},
"after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
@ -52,9 +52,11 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
if EnumPermissionsServer.BACKUP not in server_permissions:
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
self.finish_json(
200, self.controller.management.get_backups_by_server(server_id)
)
def patch(self, backup_id: str):
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
@ -80,8 +82,6 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
"error_data": str(e),
},
)
backup_conf = self.controller.management.get_backup_config(backup_id)
server_id = backup_conf["server_id"]
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})

View File

@ -67,7 +67,7 @@
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path"
<input type="text" class="form-control" name="backup_location" id="backup_location"
value="{{ data['backup_config']['backup_location'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
{% end %}
@ -107,14 +107,14 @@
<input type="checkbox" class="form-check-input" id="before-check" name="before-check" checked>{{
translate('serverBackups', 'before', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before"
<input type="text" class="form-control" name="before" id="backup_before"
value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check">{{
translate('serverBackups', 'before', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before" value=""
<input type="text" class="form-control" name="before" id="backup_before" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
@ -124,14 +124,14 @@
<input type="checkbox" class="form-check-input" id="after-check" name="after-check" checked>{{
translate('serverBackups', 'after', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after"
<input type="text" class="form-control" name="after" id="backup_after"
value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check">{{
translate('serverBackups', 'after', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after" value=""
<input type="text" class="form-control" name="after" id="backup_after" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
@ -206,7 +206,8 @@
</a>
<br>
<br>
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_location'] }}"
<button data-file="{{ backup['path'] }}"
data-backup_location="{{ data['backup_config']['backup_location'] }}"
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }}
@ -300,6 +301,7 @@
<script>
const server_id = new URLSearchParams(document.location.search).get('id')
const backup_id = new URLSearchParams(document.location.search).get('backup_id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
@ -310,7 +312,7 @@
async function backup_started() {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server/${backup_id}`, {
method: 'POST',
headers: {
'X-XSRFToken': token
@ -339,7 +341,7 @@
async function del_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({ "filename": filename })
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
let res = await fetch(`/api/v2/servers/${id}/backups/backup/${backup_id}/`, {
method: 'DELETE',
headers: {
'token': token,
@ -364,7 +366,7 @@
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false
});
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
let res = await fetch(`/api/v2/servers/${id}/backups/backup/${backup_id}/`, {
method: 'POST',
headers: {
'token': token,
@ -400,7 +402,14 @@
});
function replacer(key, value) {
if (key != "backup_before" && key != "backup_after") {
if (key === "exclusions") {
if (value == 0) {
return []
} else {
return value
}
}
if (key != "before" && key != "after") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
@ -426,22 +435,20 @@
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.compress = $("#compress").prop('checked');
formDataObject.shutdown = $("#shutdown").prop('checked');
let excluded = [];
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
if ($("#root_files_button").hasClass("clicked")) {
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
formDataObject.exclusions = excluded;
}
delete formDataObject.root_path
console.log(excluded);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${server_id}/backups/`, {
let res = await fetch(`/api/v2/servers/${server_id}/backups/backup/${backup_id}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
@ -461,7 +468,7 @@
});
try {
if ($('#backup_path').val() == '') {
if ($('#backup_location').val() == '') {
console.log('true')
try {
document.getElementById('backup_now_button').disabled = true;
@ -502,7 +509,7 @@
$(".del_button").click(function () {
var file_to_del = $(this).data("file");
var backup_path = $(this).data('backup_path');
var backup_location = $(this).data('backup_location');
console.log("file to delete is" + file_to_del);
@ -520,7 +527,7 @@
callback: function (result) {
console.log(result);
if (result == true) {
var full_path = backup_path + '/' + file_to_del;
var full_path = backup_location + '/' + file_to_del;
del_backup(file_to_del, server_id);
}
}