Files
MokoSuiteBackup/source/packages/com_mokosuitebackup/tmpl/backup/default.php
T
Jonathan Miller 4213def0ad feat: backup archive browser — view files without extracting (#59)
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
2026-06-22 22:21:49 -05:00

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>