Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3283c323f | |||
| 27dded6c62 | |||
| e465dfa6ee | |||
| 3ac0318ba3 | |||
| 17e4625448 | |||
| eb748323f7 | |||
| bc3085f74b | |||
| f66100f74f | |||
| be8b1f73bf | |||
| 0f2c4fc238 | |||
| d0fe641d5c | |||
| 4a2520a43b | |||
| 54c3a6e2e9 | |||
| a27ec0f0b9 | |||
| a7c30ad67c | |||
| ee21f7a373 |
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.33.00
|
# VERSION: 01.35.02
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+15
-18
@@ -1,6 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [01.35.00] --- 2026-06-23
|
||||||
|
|
||||||
|
## [01.35.00] --- 2026-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- SFTP remote storage with SSH key file authentication — key stored securely in database
|
||||||
|
- CLI restore options: --files-only, --db-only, --no-preserve-config, --password
|
||||||
|
|
||||||
|
## [01.34.00] --- 2026-06-23
|
||||||
|
|
||||||
|
## [01.34.00] --- 2026-06-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Dashboard: snapshot widget, backup trend chart (30 days), and storage breakdown by profile (#61)
|
||||||
|
|
||||||
## [01.33.00] --- 2026-06-23
|
## [01.33.00] --- 2026-06-23
|
||||||
|
|
||||||
## [01.33.00] --- 2026-06-23
|
## [01.33.00] --- 2026-06-23
|
||||||
@@ -9,21 +24,3 @@
|
|||||||
- Backup comparison: select two backups to view side-by-side size, file count, and duration differences (#64)
|
- 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)
|
- Selective article restore: browse articles inside a snapshot and restore individual items (#58)
|
||||||
- Archive browser: view files inside a backup without extracting (#59)
|
- 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)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoSuiteBackup
|
# MokoSuiteBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.33.00 -->
|
<!-- VERSION: 01.35.02 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ class BackupsController extends ApiController
|
|||||||
// Strip sensitive credentials before serialization
|
// Strip sensitive credentials before serialization
|
||||||
$sensitiveFields = [
|
$sensitiveFields = [
|
||||||
'ftp_password', 'ftp_username',
|
'ftp_password', 'ftp_username',
|
||||||
|
'sftp_password', 'sftp_key_data', 'sftp_passphrase',
|
||||||
's3_access_key', 's3_secret_key',
|
's3_access_key', 's3_secret_key',
|
||||||
'gdrive_client_secret', 'gdrive_refresh_token',
|
'gdrive_client_secret', 'gdrive_refresh_token',
|
||||||
'encryption_password', 'ntfy_token',
|
'encryption_password', 'ntfy_token',
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
>
|
>
|
||||||
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
|
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
|
||||||
<option value="ftp">COM_MOKOJOOMBACKUP_REMOTE_FTP</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="google_drive">COM_MOKOJOOMBACKUP_REMOTE_GDRIVE</option>
|
||||||
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
|
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
|
||||||
</field>
|
</field>
|
||||||
@@ -174,6 +175,69 @@
|
|||||||
<option value="1">JYES</option>
|
<option value="1">JYES</option>
|
||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
</field>
|
</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_password"
|
||||||
|
type="password"
|
||||||
|
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD"
|
||||||
|
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC"
|
||||||
|
maxlength="255"
|
||||||
|
showon="remote_storage:sftp"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="sftp_key_data"
|
||||||
|
type="textarea"
|
||||||
|
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY"
|
||||||
|
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC"
|
||||||
|
rows="6"
|
||||||
|
cols="60"
|
||||||
|
filter="raw"
|
||||||
|
showon="remote_storage:sftp"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
<fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION">
|
<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_SCHEDULED_TASKS="Scheduled Tasks"
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
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
|
; Backups view
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||||
@@ -236,7 +242,25 @@ 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."
|
COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM="No checksum stored for this backup. Only backups created after this update can be verified."
|
||||||
|
|
||||||
; S3 storage
|
; S3 storage
|
||||||
|
COM_MOKOJOOMBACKUP_REMOTE_SFTP="SFTP (SSH File Transfer)"
|
||||||
COM_MOKOJOOMBACKUP_REMOTE_S3="Amazon S3 / S3-Compatible"
|
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="Paste the contents of 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_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_FIELDSET_S3="S3 Storage Settings"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT="S3 Endpoint"
|
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."
|
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">
|
<extension type="component" method="upgrade">
|
||||||
<name>MokoSuiteBackup</name>
|
<name>MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
|||||||
`ftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
`ftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
||||||
`ftp_passive` TINYINT(1) NOT NULL DEFAULT 1,
|
`ftp_passive` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`ftp_ssl` TINYINT(1) NOT NULL DEFAULT 0,
|
`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_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_id` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`gdrive_client_secret` VARCHAR(255) NOT NULL DEFAULT '',
|
`gdrive_client_secret` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`gdrive_refresh_token` VARCHAR(512) 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`;
|
||||||
@@ -453,6 +453,7 @@ class BackupEngine
|
|||||||
{
|
{
|
||||||
return match ($type) {
|
return match ($type) {
|
||||||
'ftp' => new FtpUploader($profile),
|
'ftp' => new FtpUploader($profile),
|
||||||
|
'sftp' => new SftpUploader($profile),
|
||||||
'google_drive' => new GoogleDriveUploader($profile),
|
'google_drive' => new GoogleDriveUploader($profile),
|
||||||
's3' => new S3Uploader($profile),
|
's3' => new S3Uploader($profile),
|
||||||
default => throw new \InvalidArgumentException('Unknown remote storage type: ' . $type),
|
default => throw new \InvalidArgumentException('Unknown remote storage type: ' . $type),
|
||||||
|
|||||||
@@ -278,6 +278,21 @@ class PreflightCheck
|
|||||||
|
|
||||||
break;
|
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':
|
case 'google_drive':
|
||||||
if (empty($profile->gdrive_client_id) || empty($profile->gdrive_client_secret)) {
|
if (empty($profile->gdrive_client_id) || empty($profile->gdrive_client_secret)) {
|
||||||
$this->warnings[] = 'Google Drive OAuth credentials are not configured — remote upload will fail';
|
$this->warnings[] = 'Google Drive OAuth credentials are not configured — remote upload will fail';
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<?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';
|
||||||
|
|
||||||
|
if (file_put_contents($keyFile, $this->keyData) === 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) {
|
$uploader = match ($session->remoteStorage) {
|
||||||
'ftp' => new FtpUploader($profile),
|
'ftp' => new FtpUploader($profile),
|
||||||
|
'sftp' => new SftpUploader($profile),
|
||||||
'google_drive' => new GoogleDriveUploader($profile),
|
'google_drive' => new GoogleDriveUploader($profile),
|
||||||
's3' => new S3Uploader($profile),
|
's3' => new S3Uploader($profile),
|
||||||
default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage),
|
default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage),
|
||||||
|
|||||||
@@ -198,6 +198,90 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
return false;
|
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.
|
* Get published backup profiles for the quick-action selector.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,18 +24,26 @@ class HtmlView extends BaseHtmlView
|
|||||||
public array $systemHealth = [];
|
public array $systemHealth = [];
|
||||||
public array $profiles = [];
|
public array $profiles = [];
|
||||||
public bool $defaultDirWarning = false;
|
public bool $defaultDirWarning = false;
|
||||||
|
public ?object $latestSnapshot = null;
|
||||||
|
public int $snapshotCount = 0;
|
||||||
|
public array $backupTrend = [];
|
||||||
|
public array $storageByProfile = [];
|
||||||
|
|
||||||
public function display($tpl = null): void
|
public function display($tpl = null): void
|
||||||
{
|
{
|
||||||
/** @var \Joomla\Component\MokoSuiteBackup\Administrator\Model\DashboardModel $model */
|
/** @var \Joomla\Component\MokoSuiteBackup\Administrator\Model\DashboardModel $model */
|
||||||
$model = $this->getModel();
|
$model = $this->getModel();
|
||||||
|
|
||||||
$this->lastBackup = $model->getLastBackup();
|
$this->lastBackup = $model->getLastBackup();
|
||||||
$this->nextScheduled = $model->getNextScheduled();
|
$this->nextScheduled = $model->getNextScheduled();
|
||||||
$this->stats = $model->getStats();
|
$this->stats = $model->getStats();
|
||||||
$this->systemHealth = $model->getSystemHealth();
|
$this->systemHealth = $model->getSystemHealth();
|
||||||
$this->profiles = $model->getProfiles();
|
$this->profiles = $model->getProfiles();
|
||||||
$this->defaultDirWarning = $model->isUsingDefaultBackupDir();
|
$this->defaultDirWarning = $model->isUsingDefaultBackupDir();
|
||||||
|
$this->latestSnapshot = $model->getLatestSnapshot();
|
||||||
|
$this->snapshotCount = $model->getSnapshotCount();
|
||||||
|
$this->backupTrend = $model->getBackupTrend();
|
||||||
|
$this->storageByProfile = $model->getStorageByProfile();
|
||||||
|
|
||||||
$this->addToolbar();
|
$this->addToolbar();
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,122 @@ document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
|||||||
});
|
});
|
||||||
</script>
|
</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 -->
|
<!-- Row 2: Quick Actions -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>Action Log - MokoSuiteBackup</name>
|
<name>Action Log - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>Console - MokoSuiteBackup</name>
|
<name>Console - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
|
|||||||
use Joomla\Console\Command\AbstractCommand;
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ class RestoreCommand extends AbstractCommand
|
|||||||
{
|
{
|
||||||
$this->setDescription('Restore a backup by record ID');
|
$this->setDescription('Restore a backup by record ID');
|
||||||
$this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore');
|
$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
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
@@ -85,8 +90,22 @@ class RestoreCommand extends AbstractCommand
|
|||||||
require_once $engineFile;
|
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();
|
$engine = new RestoreEngine();
|
||||||
$result = $engine->restore($recordId);
|
$result = $engine->restore($recordId, $restoreFiles, $restoreDb, $preserveConfig, $password);
|
||||||
|
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
$io->success($result['message']);
|
$io->success($result['message']);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoSuiteBackup</name>
|
<name>Content - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>Quick Icon - MokoSuiteBackup</name>
|
<name>Quick Icon - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoSuiteBackup</name>
|
<name>System - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>Task - MokoSuiteBackup</name>
|
<name>Task - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoSuiteBackup</name>
|
<name>Web Services - MokoSuiteBackup</name>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteBackup</name>
|
<name>Package - MokoSuiteBackup</name>
|
||||||
<packagename>mokosuitebackup</packagename>
|
<packagename>mokosuitebackup</packagename>
|
||||||
<version>01.33.00</version>
|
<version>01.35.02</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
Reference in New Issue
Block a user