447f7b572e
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 40s
Generic: Project CI / Lint & Validate (pull_request) Successful in 52s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m8s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Retention (RetentionManager and the plugin's hourly cleanup) only ever deleted local files; remote archives grew unbounded. Add an idempotent delete() to RemoteUploaderInterface + all four uploaders and have RetentionManager remove each pruned archive from the profile's enabled remotes (best-effort; failures logged). The shared restore.php is left in place since every backup overwrites it. (#229) Consolidate duplicated plumbing (#230): - New RemoteUploaderFactory replaces the createUploaderFromParams copy duplicated in BackupEngine and SteppedBackupEngine. - RetentionManager becomes the single retention authority (global-default fallback + pruneOrphans()); the system plugin delegates to it and its duplicate doCleanup()/deleteBackupRecord() logic is removed. - Backend controller, API controller and legacy cli/mokosuitebackup.php now run through the shared BackupRunner instead of BackupEngine directly. Refs #229 #230 Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
155 lines
4.1 KiB
PHP
155 lines
4.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
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Api\Controller;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\MVC\Controller\ApiController;
|
|
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
|
|
|
|
class BackupsController extends ApiController
|
|
{
|
|
protected $contentType = 'backups';
|
|
protected $default_view = 'backups';
|
|
|
|
/**
|
|
* Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup)
|
|
*/
|
|
public function backup(): static
|
|
{
|
|
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
|
$this->app->setHeader('status', 403);
|
|
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
$data = json_decode($this->input->json->getRaw(), true) ?: [];
|
|
|
|
$profileId = (int) ($data['profile'] ?? 1);
|
|
$description = $data['description'] ?? 'API backup ' . date('Y-m-d H:i:s');
|
|
|
|
$result = (new BackupRunner())->run($profileId, $description, 'api');
|
|
|
|
if ($result['success']) {
|
|
$this->app->setHeader('status', 200);
|
|
echo json_encode(['data' => $result]);
|
|
} else {
|
|
$this->app->setHeader('status', 500);
|
|
echo json_encode(['errors' => [['title' => $result['message']]]]);
|
|
}
|
|
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download)
|
|
*/
|
|
public function download(): static
|
|
{
|
|
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) {
|
|
$this->app->setHeader('status', 403);
|
|
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
$id = $this->input->getInt('id', 0);
|
|
|
|
$model = $this->getModel('Backup', 'Administrator');
|
|
$item = $model->getItem($id);
|
|
|
|
if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) {
|
|
$this->app->setHeader('status', 404);
|
|
echo json_encode(['errors' => [['title' => 'Backup file not found']]]);
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Stream as binary download instead of base64 to avoid memory exhaustion
|
|
while (@ob_end_clean()) {
|
|
// clear all buffers
|
|
}
|
|
|
|
$filename = basename($item->archivename ?? $item->absolute_path);
|
|
$filesize = filesize($item->absolute_path);
|
|
$contentType = str_ends_with($filename, '.tar.gz')
|
|
? 'application/gzip'
|
|
: 'application/zip';
|
|
|
|
header('Content-Type: ' . $contentType);
|
|
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
|
header('Content-Length: ' . $filesize);
|
|
header('Cache-Control: no-cache, must-revalidate');
|
|
|
|
readfile($item->absolute_path);
|
|
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles)
|
|
*/
|
|
public function profiles(): static
|
|
{
|
|
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
|
$this->app->setHeader('status', 403);
|
|
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
|
|
$model = $this->getModel('Profiles', 'Administrator');
|
|
$items = $model->getItems();
|
|
|
|
$data = [];
|
|
|
|
// Strip sensitive credentials before serialization
|
|
$sensitiveFields = [
|
|
'ftp_password', 'ftp_username',
|
|
'sftp_password', 'sftp_key_data', 'sftp_passphrase',
|
|
's3_access_key', 's3_secret_key',
|
|
'gdrive_client_secret', 'gdrive_refresh_token',
|
|
'encryption_password', 'ntfy_token',
|
|
];
|
|
|
|
foreach ($items as $item) {
|
|
$safe = clone $item;
|
|
|
|
foreach ($sensitiveFields as $field) {
|
|
if (isset($safe->$field) && $safe->$field !== '') {
|
|
$safe->$field = '***';
|
|
}
|
|
}
|
|
|
|
$data[] = [
|
|
'type' => 'profiles',
|
|
'id' => $safe->id,
|
|
'attributes' => $safe,
|
|
];
|
|
}
|
|
|
|
$this->app->setHeader('status', 200);
|
|
echo json_encode(['data' => $data]);
|
|
$this->app->close();
|
|
|
|
return $this;
|
|
}
|
|
}
|