4213def0ad
AJAX-powered file browser in backups list and detail views: - AjaxController::browseArchive() reads ZIP/tar.gz entries - Browse button on each backup row + detail view - Modal shows file list with names and sizes (first 500 entries) Closes #59
231 lines
8.1 KiB
PHP
231 lines
8.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
$ajaxToken = Session::getFormToken();
|
|
$ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false);
|
|
?>
|
|
<div class="main-card">
|
|
<div class="card-body">
|
|
<h2><?php echo $this->escape($this->item->description); ?></h2>
|
|
|
|
<table class="table table-striped">
|
|
<tbody>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_STATUS'); ?></th>
|
|
<td>
|
|
<?php
|
|
$statusClass = match ($this->item->status) {
|
|
'complete' => 'badge bg-success',
|
|
'running' => 'badge bg-info',
|
|
'fail' => 'badge bg-danger',
|
|
default => 'badge bg-secondary',
|
|
};
|
|
?>
|
|
<span class="<?php echo $statusClass; ?>"><?php echo $this->escape($this->item->status); ?></span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE'); ?></th>
|
|
<td><?php echo $this->escape($this->item->backup_type); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ORIGIN'); ?></th>
|
|
<td><?php echo $this->escape($this->item->origin); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SIZE'); ?></th>
|
|
<td>
|
|
<?php echo HTMLHelper::_('number.bytes', $this->item->total_size); ?>
|
|
<?php if ($this->item->db_size > 0) : ?>
|
|
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_DB_SIZE'); ?>: <?php echo HTMLHelper::_('number.bytes', $this->item->db_size); ?>)</small>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_START'); ?></th>
|
|
<td><?php echo HTMLHelper::_('date', $this->item->backupstart, Text::_('DATE_FORMAT_LC2')); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_END'); ?></th>
|
|
<td><?php echo HTMLHelper::_('date', $this->item->backupend, Text::_('DATE_FORMAT_LC2')); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ARCHIVE'); ?></th>
|
|
<td><code><?php echo $this->escape($this->item->archivename); ?></code></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_PATH'); ?></th>
|
|
<td><code><?php echo $this->escape($this->item->absolute_path); ?></code></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_FILES_COUNT'); ?></th>
|
|
<td><?php echo (int) $this->item->files_count; ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_TABLES_COUNT'); ?></th>
|
|
<td><?php echo (int) $this->item->tables_count; ?></td>
|
|
</tr>
|
|
<?php if (!empty($this->item->checksum)) : ?>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_CHECKSUM'); ?></th>
|
|
<td><code class="font-monospace" style="font-size:0.85em;"><?php echo $this->escape($this->item->checksum); ?></code></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php if (!empty($this->item->remote_filename)) : ?>
|
|
<tr>
|
|
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_REMOTE'); ?></th>
|
|
<td><code><?php echo $this->escape($this->item->remote_filename); ?></code></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<?php if ($this->item->status === 'complete' && !empty($this->item->filesexist)) : ?>
|
|
<!-- Archive Browser -->
|
|
<h4 class="mt-4">
|
|
<span class="icon-folder-open" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>
|
|
</h4>
|
|
<div id="mb-detail-browse" class="bg-light rounded" style="max-height:400px; overflow-y:auto;">
|
|
<div id="mb-detail-browse-summary" class="p-2 text-muted" style="font-size:0.85rem;"></div>
|
|
<table class="table table-sm table-striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_NAME'); ?></th>
|
|
<th class="text-end" style="width:100px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_SIZE'); ?></th>
|
|
<th class="text-end" style="width:120px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_COMPRESSED'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="mb-detail-browse-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Backup Log -->
|
|
<h4 class="mt-4"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h4>
|
|
<div id="mb-detail-log" class="bg-light p-3 rounded" style="max-height:400px; overflow-y:auto;">
|
|
<pre id="mb-detail-log-body" style="white-space:pre-wrap; word-break:break-word; font-size:0.85rem; margin:0;">Loading...</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
|
var TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
|
|
|
function postAjax(params) {
|
|
var form = new URLSearchParams();
|
|
form.append(TOKEN_NAME, '1');
|
|
for (var k in params) {
|
|
if (params.hasOwnProperty(k)) {
|
|
form.append(k, params[k]);
|
|
}
|
|
}
|
|
return fetch(AJAX_URL, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
}).then(function(r) { return r.json(); });
|
|
}
|
|
|
|
// Load log
|
|
postAjax({ task: 'ajax.viewLog', id: <?php echo (int) $this->item->id; ?> })
|
|
.then(function(data) {
|
|
document.getElementById('mb-detail-log-body').textContent = data.error ? data.message : data.log;
|
|
})
|
|
.catch(function(err) {
|
|
document.getElementById('mb-detail-log-body').textContent = 'Error: ' + err.message;
|
|
});
|
|
|
|
<?php if ($this->item->status === 'complete' && !empty($this->item->filesexist)) : ?>
|
|
// Load archive contents
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var units = ['B', 'KB', 'MB', 'GB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
if (i >= units.length) i = units.length - 1;
|
|
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
}
|
|
|
|
function browseSetMessage(tbody, message, cssClass) {
|
|
tbody.textContent = '';
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.setAttribute('colspan', '3');
|
|
td.className = cssClass || 'text-center';
|
|
td.textContent = message;
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
function browseAddFileRow(tbody, file) {
|
|
var tr = document.createElement('tr');
|
|
|
|
var tdName = document.createElement('td');
|
|
tdName.style.wordBreak = 'break-all';
|
|
tdName.style.fontSize = '0.85rem';
|
|
var code = document.createElement('code');
|
|
code.textContent = file.name;
|
|
tdName.appendChild(code);
|
|
tr.appendChild(tdName);
|
|
|
|
var tdSize = document.createElement('td');
|
|
tdSize.className = 'text-end text-nowrap';
|
|
tdSize.textContent = formatFileSize(file.size);
|
|
tr.appendChild(tdSize);
|
|
|
|
var tdComp = document.createElement('td');
|
|
tdComp.className = 'text-end text-nowrap';
|
|
tdComp.textContent = formatFileSize(file.compressed_size);
|
|
tr.appendChild(tdComp);
|
|
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
var browseTbody = document.getElementById('mb-detail-browse-tbody');
|
|
var browseSummary = document.getElementById('mb-detail-browse-summary');
|
|
browseSetMessage(browseTbody, 'Loading...');
|
|
|
|
postAjax({ task: 'ajax.browseArchive', id: <?php echo (int) $this->item->id; ?> })
|
|
.then(function(data) {
|
|
if (data.error) {
|
|
browseSetMessage(browseTbody, data.message || 'Error', 'text-danger');
|
|
return;
|
|
}
|
|
browseTbody.textContent = '';
|
|
if (data.files.length === 0) {
|
|
browseSetMessage(browseTbody, 'Archive is empty', 'text-center text-muted');
|
|
} else {
|
|
for (var i = 0; i < data.files.length; i++) {
|
|
browseAddFileRow(browseTbody, data.files[i]);
|
|
}
|
|
}
|
|
var text = data.total_files + ' files, ' + formatFileSize(data.total_size) + ' uncompressed';
|
|
if (data.truncated) {
|
|
text += ' (showing first ' + data.files.length + ')';
|
|
}
|
|
browseSummary.textContent = text;
|
|
})
|
|
.catch(function(err) {
|
|
browseSetMessage(browseTbody, 'Error: ' + err.message, 'text-danger');
|
|
});
|
|
<?php endif; ?>
|
|
})();
|
|
</script>
|