Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01be74376b | |||
| bda4b0a23d | |||
| e327f9cf5c | |||
| 5b9351e5f0 | |||
| 5785e9fd1e | |||
| 1e9c8d54f4 | |||
| 7515274712 | |||
| 0be459fe34 | |||
| 11ccdbfde4 | |||
| fd517c16f3 | |||
| fe76f81b47 | |||
| 18127454b5 | |||
| 7826c315b1 | |||
| e329dbd99b | |||
| d6b3e8cff0 | |||
| 80c97620a5 | |||
| 33d852bacf | |||
| 8be0500913 | |||
| 27dded6c62 | |||
| e465dfa6ee | |||
| 3ac0318ba3 | |||
| 17e4625448 | |||
| eb748323f7 | |||
| bc3085f74b | |||
| f66100f74f | |||
| be8b1f73bf | |||
| 0f2c4fc238 | |||
| d0fe641d5c | |||
| 4a2520a43b | |||
| 54c3a6e2e9 | |||
| a27ec0f0b9 | |||
| a7c30ad67c | |||
| ee21f7a373 |
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.33.00
|
||||
# VERSION: 01.35.05
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+12
-23
@@ -1,29 +1,18 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [01.33.00] --- 2026-06-23
|
||||
## [01.35.03] --- 2026-06-23
|
||||
|
||||
## [01.33.00] --- 2026-06-23
|
||||
## [01.35.03] --- 2026-06-23
|
||||
|
||||
## [01.35.01] --- 2026-06-23
|
||||
|
||||
## [01.35.01] --- 2026-06-23
|
||||
|
||||
## [01.35.00] --- 2026-06-23
|
||||
|
||||
## [01.35.00] --- 2026-06-23
|
||||
|
||||
### Added
|
||||
- Backup comparison: select two backups to view side-by-side size, file count, and duration differences (#64)
|
||||
- Selective article restore: browse articles inside a snapshot and restore individual items (#58)
|
||||
- Archive browser: view files inside a backup without extracting (#59)
|
||||
|
||||
## [01.32.00] --- 2026-06-22
|
||||
|
||||
## [01.32.00] --- 2026-06-22
|
||||
|
||||
### Added
|
||||
- AJAX-based stepped restore engine for large sites — prevents timeout on shared hosting (#62)
|
||||
- Email/ntfy notifications for site restores and snapshot create/restore operations (#60)
|
||||
- Scheduled task type `mokosuitebackup.snapshot` for automated content snapshots via com_scheduler (#56)
|
||||
|
||||
## [01.31.00] --- 2026-06-22
|
||||
|
||||
## [01.31.00] --- 2026-06-22
|
||||
|
||||
### Added
|
||||
- REST API endpoints for content snapshots: list, create, restore, delete, download (#54)
|
||||
- Automatic archive integrity verification after backup creation (#65)
|
||||
- CLI command `mokosuitebackup:snapshot` for create, restore, list, and delete operations (#55)
|
||||
- SFTP remote storage with SSH key file authentication — key stored securely in database
|
||||
- CLI restore options: --files-only, --db-only, --no-preserve-config, --password
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteBackup
|
||||
|
||||
<!-- VERSION: 01.33.00 -->
|
||||
<!-- VERSION: 01.35.05 -->
|
||||
|
||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ class BackupsController extends ApiController
|
||||
// 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',
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
default="none"
|
||||
>
|
||||
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
|
||||
<option value="ftp">COM_MOKOJOOMBACKUP_REMOTE_FTP</option>
|
||||
<option value="sftp">COM_MOKOJOOMBACKUP_REMOTE_SFTP</option>
|
||||
<option value="google_drive">COM_MOKOJOOMBACKUP_REMOTE_GDRIVE</option>
|
||||
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
|
||||
</field>
|
||||
@@ -174,6 +174,80 @@
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<!-- SFTP fields (shown when remote_storage = sftp) -->
|
||||
<field
|
||||
name="sftp_host"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
<field
|
||||
name="sftp_port"
|
||||
type="number"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT_DESC"
|
||||
default="22"
|
||||
min="1"
|
||||
max="65535"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
<field
|
||||
name="sftp_username"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
<field
|
||||
name="sftp_auth_type"
|
||||
type="list"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC"
|
||||
default="key"
|
||||
showon="remote_storage:sftp"
|
||||
>
|
||||
<option value="password">COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD</option>
|
||||
<option value="key">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY</option>
|
||||
<option value="key_passphrase">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE</option>
|
||||
</field>
|
||||
<field
|
||||
name="sftp_password"
|
||||
type="password"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:password"
|
||||
/>
|
||||
<field
|
||||
name="sftp_key_data"
|
||||
type="SshKey"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC"
|
||||
filter="raw"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:key,key_passphrase"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="sftp_passphrase"
|
||||
type="password"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC"
|
||||
maxlength="255"
|
||||
showon="remote_storage:sftp[AND]sftp_auth_type:key_passphrase"
|
||||
/>
|
||||
<field
|
||||
name="sftp_path"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH_DESC"
|
||||
default="/backups"
|
||||
maxlength="512"
|
||||
showon="remote_storage:sftp"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION">
|
||||
|
||||
@@ -33,6 +33,12 @@ COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_SNAPSHOTS="Content Snapshots"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_VIEW_ALL="View All"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LATEST_SNAPSHOT="Latest"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_SNAPSHOTS="No snapshots yet. Create one from the Content Snapshots view."
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN="Storage by Profile"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||
|
||||
; Backups view
|
||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||
@@ -236,7 +242,35 @@ COM_MOKOJOOMBACKUP_VERIFY_FAILED="INTEGRITY CHECK FAILED — archive has been mo
|
||||
COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM="No checksum stored for this backup. Only backups created after this update can be verified."
|
||||
|
||||
; S3 storage
|
||||
COM_MOKOJOOMBACKUP_REMOTE_SFTP="SFTP (SSH File Transfer)"
|
||||
COM_MOKOJOOMBACKUP_REMOTE_S3="Amazon S3 / S3-Compatible"
|
||||
|
||||
; SFTP fields
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_SFTP="SFTP Settings"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST="SFTP Host"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST_DESC="SFTP server hostname or IP address"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT="SFTP Port"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT_DESC="SSH port (default: 22)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME="SSH Username"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication. Leave blank if using a key file."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY="SSH Private Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload or paste your SSH private key (e.g. id_rsa or id_ed25519). The key is stored securely in the database and written to a temp file with 0600 permissions only during upload, then deleted. Leave blank to use password authentication."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD="Upload Key File"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_NONE="No key file"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR="Remove Key"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE="Authentication Type"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC="Choose how to authenticate with the SFTP server."
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD="Password"
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY="Key File"
|
||||
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE="Key File + Passphrase"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE="Key Passphrase"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC="Passphrase for the private key, if encrypted. Leave blank for unencrypted keys."
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH="Remote Path"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH_DESC="Directory on the remote server to upload backups to"
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_S3="S3 Storage Settings"
|
||||
COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT="S3 Endpoint"
|
||||
COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT_DESC="S3 API endpoint URL. Leave blank for AWS S3. For Wasabi, MinIO, Backblaze B2, enter their endpoint URL."
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -19,6 +19,14 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`ftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
||||
`ftp_passive` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`ftp_ssl` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`sftp_host` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22,
|
||||
`sftp_username` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key',
|
||||
`sftp_password` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_key_data` MEDIUMTEXT,
|
||||
`sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
||||
`gdrive_client_id` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`gdrive_client_secret` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`gdrive_refresh_token` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- MokoSuiteBackup 01.35.00 — SFTP support with key file storage
|
||||
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
ADD COLUMN `sftp_host` VARCHAR(255) NOT NULL DEFAULT '' AFTER `ftp_ssl`,
|
||||
ADD COLUMN `sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22 AFTER `sftp_host`,
|
||||
ADD COLUMN `sftp_username` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_port`,
|
||||
ADD COLUMN `sftp_password` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_username`,
|
||||
ADD COLUMN `sftp_key_data` MEDIUMTEXT AFTER `sftp_password`,
|
||||
ADD COLUMN `sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_key_data`,
|
||||
ADD COLUMN `sftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups' AFTER `sftp_passphrase`;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- MokoSuiteBackup 01.36.00 — SFTP auth type column
|
||||
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
ADD COLUMN `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key' AFTER `sftp_username`;
|
||||
@@ -453,6 +453,7 @@ class BackupEngine
|
||||
{
|
||||
return match ($type) {
|
||||
'ftp' => new FtpUploader($profile),
|
||||
'sftp' => new SftpUploader($profile),
|
||||
'google_drive' => new GoogleDriveUploader($profile),
|
||||
's3' => new S3Uploader($profile),
|
||||
default => throw new \InvalidArgumentException('Unknown remote storage type: ' . $type),
|
||||
|
||||
@@ -278,6 +278,21 @@ class PreflightCheck
|
||||
|
||||
break;
|
||||
|
||||
case 'sftp':
|
||||
if (empty($profile->sftp_host)) {
|
||||
$this->warnings[] = 'SFTP host is not configured — remote upload will fail';
|
||||
}
|
||||
|
||||
if (empty($profile->sftp_username)) {
|
||||
$this->warnings[] = 'SFTP username is not configured — remote upload will fail';
|
||||
}
|
||||
|
||||
if (empty($profile->sftp_key_data) && empty($profile->sftp_password)) {
|
||||
$this->warnings[] = 'SFTP requires either a private key or password — remote upload will fail';
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'google_drive':
|
||||
if (empty($profile->gdrive_client_id) || empty($profile->gdrive_client_secret)) {
|
||||
$this->warnings[] = 'Google Drive OAuth credentials are not configured — remote upload will fail';
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<?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
|
||||
*
|
||||
* SFTP uploader using the system sftp/scp binary with SSH key authentication.
|
||||
*
|
||||
* The private key is stored in the database (profile column) and written
|
||||
* to a temp file with 0600 permissions at upload time, then deleted.
|
||||
* This avoids leaving key files on the filesystem permanently.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
class SftpUploader implements RemoteUploaderInterface
|
||||
{
|
||||
private string $host;
|
||||
private int $port;
|
||||
private string $username;
|
||||
private string $keyData;
|
||||
private string $passphrase;
|
||||
private string $password;
|
||||
private string $remotePath;
|
||||
|
||||
public function __construct(object $profile)
|
||||
{
|
||||
$this->host = $profile->sftp_host ?? '';
|
||||
$this->port = (int) ($profile->sftp_port ?? 22);
|
||||
$this->username = $profile->sftp_username ?? '';
|
||||
$this->keyData = $profile->sftp_key_data ?? '';
|
||||
$this->passphrase = $profile->sftp_passphrase ?? '';
|
||||
$this->password = $profile->sftp_password ?? '';
|
||||
$this->remotePath = rtrim($profile->sftp_path ?? '/backups', '/');
|
||||
}
|
||||
|
||||
public function upload(string $localPath, string $remoteName): array
|
||||
{
|
||||
if (empty($this->host)) {
|
||||
return ['success' => false, 'message' => 'SFTP host is not configured'];
|
||||
}
|
||||
|
||||
if (empty($this->username)) {
|
||||
return ['success' => false, 'message' => 'SFTP username is not configured'];
|
||||
}
|
||||
|
||||
if (empty($this->keyData) && empty($this->password)) {
|
||||
return ['success' => false, 'message' => 'SFTP requires either a private key or password'];
|
||||
}
|
||||
|
||||
$keyFile = null;
|
||||
|
||||
try {
|
||||
/* Write key to temp file if using key auth */
|
||||
if (!empty($this->keyData)) {
|
||||
$keyFile = $this->writeTempKey();
|
||||
}
|
||||
|
||||
/* Ensure remote directory exists */
|
||||
$this->ensureRemoteDir($keyFile);
|
||||
|
||||
/* Upload via scp */
|
||||
$remoteTarget = $this->username . '@' . $this->host . ':' . $this->remotePath . '/' . $remoteName;
|
||||
$cmd = $this->buildScpCommand($localPath, $remoteTarget, $keyFile);
|
||||
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec($cmd . ' 2>&1', $output, $exitCode);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
$errorMsg = implode("\n", $output);
|
||||
throw new \RuntimeException('scp failed (exit ' . $exitCode . '): ' . $errorMsg);
|
||||
}
|
||||
|
||||
/* Verify upload by checking remote file size */
|
||||
$remoteFile = $this->remotePath . '/' . $remoteName;
|
||||
$remoteSize = $this->getRemoteFileSize($remoteFile, $keyFile);
|
||||
$localSize = filesize($localPath);
|
||||
|
||||
if ($remoteSize > 0 && $remoteSize !== $localSize) {
|
||||
throw new \RuntimeException(
|
||||
'Size mismatch after upload: local=' . $localSize . ' remote=' . $remoteSize
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Uploaded via SFTP: ' . $remoteFile,
|
||||
'remote_path' => $remoteFile,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => 'SFTP upload failed: ' . $e->getMessage()];
|
||||
} finally {
|
||||
$this->cleanupTempKey($keyFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
if (empty($this->host)) {
|
||||
return ['success' => false, 'message' => 'SFTP host is not configured'];
|
||||
}
|
||||
|
||||
$keyFile = null;
|
||||
|
||||
try {
|
||||
if (!empty($this->keyData)) {
|
||||
$keyFile = $this->writeTempKey();
|
||||
}
|
||||
|
||||
$cmd = $this->buildSshCommand('echo "MokoSuiteBackup connection test OK" && hostname', $keyFile);
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec($cmd . ' 2>&1', $output, $exitCode);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
return ['success' => false, 'message' => 'SSH connection failed: ' . implode(' ', $output)];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Connected to ' . $this->host . ' — ' . implode(' ', $output),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => 'Connection test failed: ' . $e->getMessage()];
|
||||
} finally {
|
||||
$this->cleanupTempKey($keyFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the private key from the database to a temp file with 0600 permissions.
|
||||
*/
|
||||
private function writeTempKey(): string
|
||||
{
|
||||
$tmpDir = sys_get_temp_dir();
|
||||
$keyFile = $tmpDir . '/mokobackup-sftp-' . bin2hex(random_bytes(8)) . '.key';
|
||||
|
||||
/* Key is stored base64-encoded in the database — decode before writing */
|
||||
$keyContent = base64_decode($this->keyData, true);
|
||||
|
||||
if ($keyContent === false) {
|
||||
/* Fallback: might be raw PEM (legacy or paste) */
|
||||
$keyContent = $this->keyData;
|
||||
}
|
||||
|
||||
if (file_put_contents($keyFile, $keyContent) === false) {
|
||||
throw new \RuntimeException('Cannot write temporary SSH key file');
|
||||
}
|
||||
|
||||
chmod($keyFile, 0600);
|
||||
|
||||
return $keyFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the temp key file.
|
||||
*/
|
||||
private function cleanupTempKey(?string $keyFile): void
|
||||
{
|
||||
if ($keyFile !== null && is_file($keyFile)) {
|
||||
unlink($keyFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the remote directory exists via ssh mkdir -p.
|
||||
*/
|
||||
private function ensureRemoteDir(?string $keyFile): void
|
||||
{
|
||||
$escapedPath = escapeshellarg($this->remotePath);
|
||||
$cmd = $this->buildSshCommand('mkdir -p ' . $escapedPath, $keyFile);
|
||||
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec($cmd . ' 2>&1', $output, $exitCode);
|
||||
|
||||
/* mkdir -p exits 0 even if dir already exists, so only fail on non-zero */
|
||||
if ($exitCode !== 0) {
|
||||
throw new \RuntimeException('Cannot create remote directory: ' . implode(' ', $output));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote file size via ssh stat.
|
||||
*/
|
||||
private function getRemoteFileSize(string $remotePath, ?string $keyFile): int
|
||||
{
|
||||
$escapedPath = escapeshellarg($remotePath);
|
||||
$cmd = $this->buildSshCommand('stat -c %s ' . $escapedPath . ' 2>/dev/null || echo -1', $keyFile);
|
||||
|
||||
$output = [];
|
||||
exec($cmd . ' 2>&1', $output);
|
||||
|
||||
$size = (int) trim(implode('', $output));
|
||||
|
||||
return $size > 0 ? $size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an scp command string with proper SSH options.
|
||||
*/
|
||||
private function buildScpCommand(string $localPath, string $remoteTarget, ?string $keyFile): string
|
||||
{
|
||||
$parts = ['scp', '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes'];
|
||||
|
||||
if ($this->port !== 22) {
|
||||
$parts[] = '-P';
|
||||
$parts[] = (string) $this->port;
|
||||
}
|
||||
|
||||
if ($keyFile !== null) {
|
||||
$parts[] = '-i';
|
||||
$parts[] = escapeshellarg($keyFile);
|
||||
}
|
||||
|
||||
if (!empty($this->passphrase)) {
|
||||
/* scp doesn't natively support passphrase via CLI — requires ssh-agent or expect.
|
||||
For now, key files should be unencrypted or use ssh-agent. */
|
||||
}
|
||||
|
||||
$parts[] = escapeshellarg($localPath);
|
||||
$parts[] = escapeshellarg($remoteTarget);
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ssh command string for remote commands.
|
||||
*/
|
||||
private function buildSshCommand(string $remoteCmd, ?string $keyFile): string
|
||||
{
|
||||
$parts = ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes'];
|
||||
|
||||
if ($this->port !== 22) {
|
||||
$parts[] = '-p';
|
||||
$parts[] = (string) $this->port;
|
||||
}
|
||||
|
||||
if ($keyFile !== null) {
|
||||
$parts[] = '-i';
|
||||
$parts[] = escapeshellarg($keyFile);
|
||||
}
|
||||
|
||||
$parts[] = escapeshellarg($this->username . '@' . $this->host);
|
||||
$parts[] = escapeshellarg($remoteCmd);
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
}
|
||||
@@ -410,6 +410,7 @@ class SteppedBackupEngine
|
||||
|
||||
$uploader = match ($session->remoteStorage) {
|
||||
'ftp' => new FtpUploader($profile),
|
||||
'sftp' => new SftpUploader($profile),
|
||||
'google_drive' => new GoogleDriveUploader($profile),
|
||||
's3' => new S3Uploader($profile),
|
||||
default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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
|
||||
*
|
||||
* Custom field for SSH private key input.
|
||||
* Supports both file upload (via FileReader JS) and paste-in textarea.
|
||||
* The key content is stored in the database, not as a file on disk.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Form\FormField;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
class SshKeyField extends FormField
|
||||
{
|
||||
protected $type = 'SshKey';
|
||||
|
||||
protected function getInput(): string
|
||||
{
|
||||
$value = $this->value ?? '';
|
||||
$id = $this->id;
|
||||
$name = $this->name;
|
||||
|
||||
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
||||
|
||||
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
||||
|
||||
/* Status badge */
|
||||
if ($hasKey) {
|
||||
$html .= '<span class="badge bg-success me-2">'
|
||||
. '<span class="icon-lock" aria-hidden="true"></span> '
|
||||
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED')
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
/* File upload button */
|
||||
$html .= '<label class="btn btn-outline-secondary btn-sm" for="' . htmlspecialchars($id) . '-file">';
|
||||
$html .= '<span class="icon-upload" aria-hidden="true"></span> ';
|
||||
$html .= $hasKey ? Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE') : Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD');
|
||||
$html .= '</label>';
|
||||
$html .= '<input type="file" id="' . htmlspecialchars($id) . '-file"'
|
||||
. ' accept=".pem,.key,.openssh,.ppk,*" style="display:none;"'
|
||||
. ' onchange="mokoSshKeyFileSelected(\'' . htmlspecialchars($id) . '\', this)">';
|
||||
|
||||
$html .= '<span id="' . htmlspecialchars($id) . '-status" class="ms-2 text-muted small"></span>';
|
||||
|
||||
if ($hasKey) {
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-danger ms-2"'
|
||||
. ' onclick="mokoSshKeyClear(\'' . htmlspecialchars($id) . '\')">'
|
||||
. '<span class="icon-times" aria-hidden="true"></span> '
|
||||
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR')
|
||||
. '</button>';
|
||||
}
|
||||
|
||||
/* Hidden field — key data is NEVER rendered as visible text.
|
||||
On existing keys, we submit a sentinel value to preserve the DB value
|
||||
unless a new file is uploaded or clear is clicked. */
|
||||
if ($hasKey) {
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
||||
. ' value="__KEEP_EXISTING__">';
|
||||
} else {
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
||||
. ' value="">';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= $this->getScript();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getScript(): string
|
||||
{
|
||||
return <<<'JS'
|
||||
<script>
|
||||
function mokoSshKeyFileSelected(fieldId, input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
var file = input.files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
/* Base64 encode the key before storing in the hidden field */
|
||||
var content = e.target.result;
|
||||
var encoded = btoa(content);
|
||||
document.getElementById(fieldId).value = encoded;
|
||||
var status = document.getElementById(fieldId + '-status');
|
||||
if (status) status.textContent = file.name + ' uploaded';
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function mokoSshKeyClear(fieldId) {
|
||||
document.getElementById(fieldId).value = '';
|
||||
var status = document.getElementById(fieldId + '-status');
|
||||
if (status) status.textContent = 'Key removed';
|
||||
var fileInput = document.getElementById(fieldId + '-file');
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,90 @@ class DashboardModel extends BaseDatabaseModel
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest snapshot info for the dashboard widget.
|
||||
*/
|
||||
public function getLatestSnapshot(): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuitebackup_snapshots'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||
->order($db->quoteName('created') . ' DESC');
|
||||
$db->setQuery($query, 0, 1);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshot count.
|
||||
*/
|
||||
public function getSnapshotCount(): int
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
try {
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuitebackup_snapshots'));
|
||||
$db->setQuery($query);
|
||||
|
||||
return (int) $db->loadResult();
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup size trend data for the last 30 days.
|
||||
* Returns array of {date, total_size, count, status} grouped by day.
|
||||
*/
|
||||
public function getBackupTrend(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$cutoff = date('Y-m-d', strtotime('-30 days'));
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('DATE(' . $db->quoteName('backupstart') . ') AS backup_date')
|
||||
->select('SUM(' . $db->quoteName('total_size') . ') AS day_size')
|
||||
->select('COUNT(*) AS day_count')
|
||||
->select('SUM(CASE WHEN ' . $db->quoteName('status') . ' = ' . $db->quote('fail') . ' THEN 1 ELSE 0 END) AS fail_count')
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where('DATE(' . $db->quoteName('backupstart') . ') >= ' . $db->quote($cutoff))
|
||||
->group('DATE(' . $db->quoteName('backupstart') . ')')
|
||||
->order('backup_date ASC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage breakdown by profile.
|
||||
*/
|
||||
public function getStorageByProfile(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('p.title AS profile_title')
|
||||
->select('COUNT(*) AS backup_count')
|
||||
->select('COALESCE(SUM(r.total_size), 0) AS total_size')
|
||||
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
|
||||
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
||||
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
|
||||
->group($db->quoteName('r.profile_id'))
|
||||
->order('total_size DESC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get published backup profiles for the quick-action selector.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,23 @@ class ProfileTable extends Table
|
||||
|
||||
public function store($updateNulls = true): bool
|
||||
{
|
||||
/* Handle SSH key sentinel — when __KEEP_EXISTING__ is submitted,
|
||||
preserve the current DB value instead of overwriting with the sentinel.
|
||||
This prevents the key from being exposed in the form HTML. */
|
||||
if (isset($this->sftp_key_data) && $this->sftp_key_data === '__KEEP_EXISTING__') {
|
||||
if ($this->id) {
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('sftp_key_data'))
|
||||
->from($db->quoteName($this->_tbl))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $this->id);
|
||||
$db->setQuery($query);
|
||||
$this->sftp_key_data = $db->loadResult() ?: '';
|
||||
} else {
|
||||
$this->sftp_key_data = '';
|
||||
}
|
||||
}
|
||||
|
||||
$result = parent::store($updateNulls);
|
||||
|
||||
if ($result && !empty($this->backup_dir)) {
|
||||
|
||||
@@ -24,18 +24,26 @@ class HtmlView extends BaseHtmlView
|
||||
public array $systemHealth = [];
|
||||
public array $profiles = [];
|
||||
public bool $defaultDirWarning = false;
|
||||
public ?object $latestSnapshot = null;
|
||||
public int $snapshotCount = 0;
|
||||
public array $backupTrend = [];
|
||||
public array $storageByProfile = [];
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
/** @var \Joomla\Component\MokoSuiteBackup\Administrator\Model\DashboardModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
$this->lastBackup = $model->getLastBackup();
|
||||
$this->nextScheduled = $model->getNextScheduled();
|
||||
$this->stats = $model->getStats();
|
||||
$this->systemHealth = $model->getSystemHealth();
|
||||
$this->profiles = $model->getProfiles();
|
||||
$this->lastBackup = $model->getLastBackup();
|
||||
$this->nextScheduled = $model->getNextScheduled();
|
||||
$this->stats = $model->getStats();
|
||||
$this->systemHealth = $model->getSystemHealth();
|
||||
$this->profiles = $model->getProfiles();
|
||||
$this->defaultDirWarning = $model->isUsingDefaultBackupDir();
|
||||
$this->latestSnapshot = $model->getLatestSnapshot();
|
||||
$this->snapshotCount = $model->getSnapshotCount();
|
||||
$this->backupTrend = $model->getBackupTrend();
|
||||
$this->storageByProfile = $model->getStorageByProfile();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
|
||||
@@ -109,6 +109,122 @@ document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Row 1b: Snapshot Widget -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-camera" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SNAPSHOTS'); ?>
|
||||
</h5>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=snapshots'); ?>" class="btn btn-sm btn-outline-secondary">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_VIEW_ALL'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($this->latestSnapshot) : ?>
|
||||
<?php $types = json_decode($this->latestSnapshot->content_types, true) ?: []; ?>
|
||||
<p class="mb-1">
|
||||
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LATEST_SNAPSHOT'); ?>:</strong>
|
||||
<?php echo $this->escape($this->latestSnapshot->description); ?>
|
||||
</p>
|
||||
<p class="mb-1 text-muted">
|
||||
<?php echo HTMLHelper::_('date', $this->latestSnapshot->created, Text::_('DATE_FORMAT_LC4')); ?>
|
||||
—
|
||||
<?php foreach ($types as $type) : ?>
|
||||
<span class="badge bg-secondary"><?php echo $this->escape($type); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<small class="text-muted">
|
||||
<?php echo (int) $this->latestSnapshot->articles_count; ?> articles,
|
||||
<?php echo (int) $this->latestSnapshot->categories_count; ?> categories,
|
||||
<?php echo (int) $this->latestSnapshot->modules_count; ?> modules
|
||||
— <?php echo $this->snapshotCount; ?> total snapshots
|
||||
</small>
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_SNAPSHOTS'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Breakdown by Profile -->
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN'); ?>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($this->storageByProfile)) : ?>
|
||||
<?php
|
||||
$maxSize = max(array_column($this->storageByProfile, 'total_size')) ?: 1;
|
||||
$colors = ['#0d6efd', '#198754', '#ffc107', '#dc3545', '#6f42c1', '#0dcaf0'];
|
||||
?>
|
||||
<?php foreach ($this->storageByProfile as $i => $profile) : ?>
|
||||
<?php $pct = round(($profile->total_size / $maxSize) * 100); ?>
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between small">
|
||||
<span><?php echo $this->escape($profile->profile_title ?: 'Unknown'); ?> (<?php echo (int) $profile->backup_count; ?>)</span>
|
||||
<span><?php echo HTMLHelper::_('number.bytes', $profile->total_size); ?></span>
|
||||
</div>
|
||||
<div style="background:#e9ecef; border-radius:3px; height:8px; overflow:hidden;">
|
||||
<div style="width:<?php echo $pct; ?>%; height:100%; background:<?php echo $colors[$i % count($colors)]; ?>; border-radius:3px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<p class="text-muted mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Trend (30 days) -->
|
||||
<?php if (!empty($this->backupTrend)) : ?>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<span class="icon-chart" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND'); ?>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
$maxDaySize = max(array_column($this->backupTrend, 'day_size')) ?: 1;
|
||||
?>
|
||||
<div style="display:flex; align-items:flex-end; gap:2px; height:120px; overflow-x:auto;">
|
||||
<?php foreach ($this->backupTrend as $day) : ?>
|
||||
<?php
|
||||
$barHeight = max(4, round(($day->day_size / $maxDaySize) * 100));
|
||||
$barColor = $day->fail_count > 0 ? '#dc3545' : '#198754';
|
||||
$tooltip = date('M j', strtotime($day->backup_date))
|
||||
. ' — ' . $day->day_count . ' backup(s), '
|
||||
. number_format($day->day_size / 1048576, 1) . ' MB'
|
||||
. ($day->fail_count > 0 ? ', ' . $day->fail_count . ' failed' : '');
|
||||
?>
|
||||
<div style="flex:1; min-width:8px; max-width:24px; height:<?php echo $barHeight; ?>%; background:<?php echo $barColor; ?>; border-radius:2px 2px 0 0; cursor:default;"
|
||||
title="<?php echo htmlspecialchars($tooltip); ?>"></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted"><?php echo date('M j', strtotime('-30 days')); ?></small>
|
||||
<small class="text-muted"><?php echo date('M j'); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Row 2: Quick Actions -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -17,6 +17,7 @@ use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
|
||||
use Joomla\Console\Command\AbstractCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
@@ -28,6 +29,10 @@ class RestoreCommand extends AbstractCommand
|
||||
{
|
||||
$this->setDescription('Restore a backup by record ID');
|
||||
$this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore');
|
||||
$this->addOption('files-only', null, InputOption::VALUE_NONE, 'Restore files only (skip database)');
|
||||
$this->addOption('db-only', null, InputOption::VALUE_NONE, 'Restore database only (skip files)');
|
||||
$this->addOption('no-preserve-config', null, InputOption::VALUE_NONE, 'Do not preserve current configuration.php');
|
||||
$this->addOption('password', 'p', InputOption::VALUE_REQUIRED, 'Decryption password for encrypted archives', '');
|
||||
}
|
||||
|
||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -85,8 +90,22 @@ class RestoreCommand extends AbstractCommand
|
||||
require_once $engineFile;
|
||||
}
|
||||
|
||||
$filesOnly = $input->getOption('files-only');
|
||||
$dbOnly = $input->getOption('db-only');
|
||||
$preserveConfig = !$input->getOption('no-preserve-config');
|
||||
$password = $input->getOption('password') ?: '';
|
||||
|
||||
$restoreFiles = !$dbOnly;
|
||||
$restoreDb = !$filesOnly;
|
||||
|
||||
if ($filesOnly) {
|
||||
$io->note('Restoring files only (database will not be touched)');
|
||||
} elseif ($dbOnly) {
|
||||
$io->note('Restoring database only (files will not be touched)');
|
||||
}
|
||||
|
||||
$engine = new RestoreEngine();
|
||||
$result = $engine->restore($recordId);
|
||||
$result = $engine->restore($recordId, $restoreFiles, $restoreDb, $preserveConfig, $password);
|
||||
|
||||
if ($result['success']) {
|
||||
$io->success($result['message']);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>01.33.00</version>
|
||||
<version>01.35.05</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user