{% extends ../base.html %} {% block meta %} {% end %} {% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %} {% block content %} <div class="content-wrapper"> <!-- Page Title Header Starts--> <div class="row page-title-header"> <div class="col-12"> <div class="page-header"> <h4 class="page-title"> {{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }} <br /> <small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small> </h4> </div> </div> </div> <!-- Page Title Header Ends--> {% include "parts/details_stats.html %} <div class="row"> <div class="col-sm-12 grid-margin"> <div class="card"> <div class="card-body pt-0"> <span class="d-none d-sm-block"> {% include "parts/server_controls_list.html %} </span> <span class="d-block d-sm-none"> {% include "parts/m_server_controls_list.html %} </span> <div class="row"> <div class="col-md-12 col-sm-12" style="overflow-x:auto;"> <div class="card"> <div class="card-header header-sm d-flex justify-content-between align-items-center"> <h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('serverBackups', 'backups', data['lang']) }} </h4> {% if data['user_data']['hints'] %} <span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span> {% end %} <div><a class="btn btn-info" href="/panel/add_backup?id={{ data['server_stats']['server_id']['server_id'] }}"><i class="fas fa-plus-circle"></i> {{ translate('serverBackups', 'newBackup', data['lang']) }}</a> </div> </div> <div class="card-body"> {% if len(data['backups']) == 0 %} <div style="text-align: center; color: grey;"> <h7>{{ translate('serverBackups', 'no-backup', data['lang']) }} <strong>{{ translate('serverBackups', 'newBackup',data['lang']) }}</strong>.</h7> </div> {% end %} {% if len(data['backups']) > 0 %} <div class="d-none d-lg-block"> <table class="table table-hover responsive-table" aria-label="backups list" id="backup_table" width="100%" style="table-layout:fixed;"> <thead> <tr class="rounded"> <th scope="col" style="width: 15%; min-width: 10px;">{{ translate('serverBackups', 'name', data['lang']) }} </th> <th scope="col" style="width: 60%; min-width: 50px;">{{ translate('serverBackups', 'storageLocation', data['lang']) }}</th> <th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups', 'maxBackups', data['lang']) }}</th> <th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups', 'actions', data['lang']) }}</th> </tr> </thead> <tbody> {% for backup in data['backups'] %} <tr> <td id="{{backup.backup_name}}" class="id"> <p>{{backup.backup_name}}</p> </td> <td id="{{backup.backup_location}}" class="type"> <p>{{backup.backup_location}}</p> </td> <td id="{{backup.max_backups}}" class="trigger" style="overflow: scroll; max-width: 30px;"> <p>{{backup.max_backups}}</p> </td> <td id="backup_edit" class="action"> <button onclick="window.location.href=`/panel/edit_backup?id={{ data['server_stats']['server_id']['server_id'] }}&backup_id={{backup.backup_id}}`" class="btn btn-info"> <i class="fas fa-pencil-alt"></i> </button> <button data-backup={{ backup.backup_id }} class="btn btn-danger del_button"> <i class="fas fa-trash" aria-hidden="true"></i> </button> <button data-backup={{ backup.backup_id }} data-toggle="tooltip" title="{{ translate('serverBackups', 'run', data['lang']) }}" class="btn btn-outline-warning run-backup backup_now_button"> <i class="fa-solid fa-forward"></i> </button> </td> </tr> {% end %} </tbody> </table> </div> <div class="d-block d-lg-none"> <table aria-label="backups list" class="table table-hover responsive-table" id="backup_table_mini" width="100%" style="table-layout:fixed;"> <thead> <tr class="rounded"> <th style="width: 40%; min-width: 10px;">Name </th> <th style="width: 40%; min-width: 50px;">{{ translate('serverBackups', 'edit', data['lang']) }}</th> </tr> </thead> <tbody> {% for backup in data['backups'] %} <tr> <td id="{{backup.backup_name}}" class="id"> <p>{{backup.backup_name}}</p> </td> <td id="backup_edit" class="action"> <button onclick="window.location.href=`/panel/edit_backup?id={{ data['server_stats']['server_id']['server_id'] }}&backup_id={{backup.backup_id}}`" class="btn btn-info"> <i class="fas fa-pencil-alt"></i> </button> <button data-backup={{ backup.backup_id }} class="btn btn-danger del_button"> <i class="fas fa-trash" aria-hidden="true"></i> </button> <button data-backup={{ backup.backup_id }} data-toggle="tooltip" title="{{ translate('serverBackups', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket"> <i class="fa-solid fa-vial"></i> </button> </td> </tr> {% end %} </tbody> </table> </div> {% end %} </div> </div> </div> </div> </div> </div> </div> </div> </div> <style> /* Remove default bullets */ .tree-view, .tree-nested { list-style-type: none; margin: 0; padding: 0; margin-left: 10px; } /* Style the items */ .tree-item, .files-tree-title { cursor: pointer; user-select: none; /* Prevent text selection */ } /* Create the caret/arrow with a unicode, and style it */ .tree-caret .fa-folder { display: inline-block; } .tree-caret .fa-folder-open { display: none; } /* Rotate the caret/arrow icon when clicked on (using JavaScript) */ .tree-caret-down .fa-folder { display: none; } .tree-caret-down .fa-folder-open { display: inline-block; } /* Hide the nested list */ .tree-nested { display: none; } </style> <!-- content-wrapper ends --> {% end %} {% block js %} <script> const server_id = new URLSearchParams(document.location.search).get('id') //used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } async function backup_started(backup_id) { const token = getCookie("_xsrf") console.log(backup_id) let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server/${backup_id}/`, { method: 'POST', headers: { 'X-XSRFToken': token } }); let responseData = await res.json(); if (responseData.status === "ok") { console.log(responseData); $("#backup_button").html(`<div class="progress" style="height: 15px;"> <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 { bootbox.alert({ title: responseData.status, message: responseData.error }); } return; } 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/`, { method: 'DELETE', headers: { 'token': token, }, body: contents }); let responseData = await res.json(); if (responseData.status === "ok") { window.location.reload(); } else { bootbox.alert({ "title": responseData.status, "message": responseData.error }) } } async function restore_backup(filename, id) { const token = getCookie("_xsrf") let contents = JSON.stringify({ "filename": filename }) var dialog = bootbox.dialog({ 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/`, { method: 'POST', headers: { 'token': token, }, body: contents }); let responseData = await res.json(); if (responseData.status === "ok") { window.location.href = "/panel/dashboard"; } else { bootbox.alert({ "title": responseData.status, "message": responseData.error }) } } $("#before-check").on("click", function () { if ($("#before-check:checked").val()) { $("#backup_before").css("display", "inline-block"); } else { $("#backup_before").css("display", "none"); $("#backup_before").val(""); } }); $("#after-check").on("click", function () { if ($("#after-check:checked").val()) { $("#backup_after").css("display", "inline-block"); } else { $("#backup_after").css("display", "none"); $("#backup_after").val(""); } }); function replacer(key, value) { if (key != "backup_before" && key != "backup_after") { if (typeof value == "boolean" || key === "executable_update_url") { return value } else { return (isNaN(value) ? value : +value); } } else { return value; } } $(document).ready(function () { try { if ($('#backup_path').val() == '') { console.log('true') try { document.getElementById('backup_now_button').disabled = true; } catch { } } else { document.getElementById('backup_now_button').disabled = false; } } catch { try { document.getElementById('backup_now_button').disabled = false; } catch { } } console.log("ready!"); $("#backup_config_box").hide(); $("#backup_save_note").hide(); $("#show_config").click(function () { $("#backup_config_box").toggle(); $('#backup_button').hide(); $('#backup_save_note').show(); $('#backup_data').hide(); }); $('#backup_table').DataTable({ "order": [[1, "desc"]], "paging": false, "lengthChange": false, "searching": true, "ordering": true, "info": true, "autoWidth": false, "responsive": true, }); $(".del_button").click(function () { var file_to_del = $(this).data("file"); var backup_path = $(this).data('backup_path'); console.log("file to delete is" + file_to_del); bootbox.confirm({ title: "{% raw translate('serverBackups', 'destroyBackup', data['lang']) %}", message: "{{ translate('serverBackups', 'confirmDelete', data['lang']) }}", buttons: { cancel: { label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}' }, confirm: { label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}' } }, callback: function (result) { console.log(result); if (result == true) { var full_path = backup_path + '/' + file_to_del; del_backup(file_to_del, server_id); } } }); }); $(".restore_button").click(function () { var file_to_restore = $(this).data("file"); bootbox.confirm({ title: "{{ translate('serverBackups', 'restore', data['lang']) }} " + file_to_restore, message: "{{ translate('serverBackups', 'confirmRestore', data['lang']) }}", buttons: { cancel: { label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}' }, confirm: { label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "restore", data['lang']) }}', className: 'btn-outline-danger' } }, callback: function (result) { console.log(result); if (result == true) { restore_backup(file_to_restore, server_id); } } }); }); $(".backup_now_button").click(function () { backup_started($(this).data('backup')); }); }); document.getElementById("modal-cancel").addEventListener("click", function () { document.getElementById("root_files_button").classList.remove('clicked'); document.getElementById("main-tree-div").innerHTML = '<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>' }) document.getElementById("root_files_button").addEventListener("click", function () { if ($("#root_files_button").data('server_path') != "") { if (document.getElementById('root_files_button').classList.contains('clicked')) { show_file_tree(); return; } else { document.getElementById('root_files_button').classList.add('clicked'); } path = $("#root_files_button").data('server_path') console.log($("#root_files_button").data('server_path')) const token = getCookie("_xsrf"); var dialog = bootbox.dialog({ message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>', closeButton: false }); setTimeout(function () { var x = document.querySelector('.bootbox'); if (x) { x.remove() } var x = document.querySelector('.modal-backdrop'); if (x) { x.remove() } document.getElementById('main-tree-input').setAttribute('value', path) getTreeView(path); show_file_tree(); }, 5000); } else { 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) { let path = event.target.parentElement.getAttribute("data-path"); if (document.getElementById(path).classList.contains('clicked')) { return; } else { getTreeView(path); } } async function getTreeView(path) { console.log(path) const token = getCookie("_xsrf"); let res = await fetch(`/api/v2/servers/${server_id}/files`, { method: 'POST', headers: { 'X-XSRFToken': token }, body: JSON.stringify({ "page": "backups", "path": path }), }); let responseData = await res.json(); if (responseData.status === "ok") { console.log(responseData); process_tree_response(responseData); } else { bootbox.alert({ title: responseData.status, message: responseData.error }); } } function process_tree_response(response) { let path = response.data.root_path.path; let text = `<ul class="tree-nested d-block" id="${path}ul">`; Object.entries(response.data).forEach(([key, value]) => { if (key === "root_path" || key === "db_stats") { //continue is not valid in for each. Return acts as a continue. return; } let checked = "" let dpath = value.path; let filename = key; if (value.excluded) { checked = "checked" } if (value.dir) { text += `<li class="tree-item" data-path="${dpath}"> \n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder"> <input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}> <span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)"> <i style="color: var(--info);" class="far fa-folder"></i> <i style="color: var(--info);" class="far fa-folder-open"></i> <strong>${filename}</strong> </span> </input></div><li>` } else { text += `<li class="d-block tree-ctx-item tree-file" data-path="${dpath}" data-name="${filename}" onclick=""><input type='checkbox' class="checkBoxClass excluded" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;"> <i class="far fa-file"></i></span></input>${filename}</li>` } }); text += `</ul>`; if (response.data.root_path.top) { try { document.getElementById('main-tree-div').innerHTML += text; document.getElementById('main-tree').parentElement.classList.add("clicked"); } catch { document.getElementById('files-tree').innerHTML = text; } } else { try { document.getElementById(path + "span").classList.add('tree-caret-down'); document.getElementById(path).innerHTML += text; document.getElementById(path).classList.add("clicked"); } catch { console.log("Bad") } var toggler = document.getElementById(path + "span"); if (toggler.classList.contains('files-tree-title')) { document.getElementById(path + "span").addEventListener("click", function caretListener() { document.getElementById(path + "ul").classList.toggle("d-block"); document.getElementById(path + "span").classList.toggle("tree-caret-down"); }); } } } function getToggleMain(event) { path = event.target.parentElement.getAttribute('data-path'); document.getElementById("files-tree").classList.toggle("d-block"); document.getElementById(path + "span").classList.toggle("tree-caret-down"); document.getElementById(path + "span").classList.toggle("tree-caret"); } function show_file_tree() { $("#dir_select").modal(); } </script> {% end %}