From 68605ffc0559ef9a7965aedb00b07ce57a6f724e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 4 Jul 2026 13:22:44 -0500 Subject: [PATCH] refactor(remote): remove legacy single-remote storage in favor of remotes table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the per-profile remote_storage column and all legacy FTP/SFTP/S3/ Google Drive credential columns. Remote destinations are now sourced exclusively from #__mokosuitebackup_remotes (multi-remote), which is created at install time — so the backward-compat fallback branches in BackupEngine, SteppedBackupEngine and loadRemoteDestinations are removed. - sql: drop 26 legacy columns (install.mysql.sql + 02.52.25.sql migration) - forms/profile.xml: remove legacy remote fields and ftp/gdrive/s3 fieldsets - tmpl/profile/edit.php: drop legacy UI, add save-first prompt, use getOrCreateInstance for the modal, read item.params (was item.config) - PreflightCheck: validate credentials from the remotes table; curl warning now applies to ntfy only - SteppedSession: drop remoteStorage property - language: add backup-record delete-count strings - script.php: simplify postflight license-key prompt --- .../com_mokosuitebackup/forms/profile.xml | 247 ------------------ .../language/en-US/com_mokosuitebackup.ini | 4 + .../com_mokosuitebackup/sql/install.mysql.sql | 26 -- .../sql/updates/mysql/02.52.25.sql | 27 ++ .../src/Engine/BackupEngine.php | 89 +------ .../src/Engine/PreflightCheck.php | 101 +++---- .../src/Engine/SteppedBackupEngine.php | 190 ++++---------- .../src/Engine/SteppedSession.php | 1 - .../com_mokosuitebackup/tmpl/profile/edit.php | 31 +-- source/script.php | 46 +--- 10 files changed, 167 insertions(+), 595 deletions(-) create mode 100644 source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql diff --git a/source/packages/com_mokosuitebackup/forms/profile.xml b/source/packages/com_mokosuitebackup/forms/profile.xml index 81a46de..d7fa692 100644 --- a/source/packages/com_mokosuitebackup/forms/profile.xml +++ b/source/packages/com_mokosuitebackup/forms/profile.xml @@ -206,25 +206,6 @@
- - - - - - - JYES - - - - - - - - - - - - - -
@@ -408,157 +314,4 @@ />
-
- - - - - - - - - - - - - -
- -
- - - - -
- -
- - - - - - -
diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini index db5e85b..fc50e39 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -126,6 +126,10 @@ COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled." ; Backup status COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning" +; Delete feedback +COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted." +COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted." + ; ACL - Cancel COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup" COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files." diff --git a/source/packages/com_mokosuitebackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql index 705ebfd..8b16d99 100644 --- a/source/packages/com_mokosuitebackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -11,32 +11,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` ( `exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude', `exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude', `exclude_tables` TEXT NOT NULL COMMENT 'Newline-separated table names to exclude', - `remote_storage` VARCHAR(20) NOT NULL DEFAULT 'none' COMMENT 'none, ftp, google_drive, s3', - `ftp_host` VARCHAR(255) NOT NULL DEFAULT '', - `ftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 21, - `ftp_username` VARCHAR(255) NOT NULL DEFAULT '', - `ftp_password` VARCHAR(255) NOT NULL DEFAULT '', - `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 '', - `gdrive_folder_id` VARCHAR(255) NOT NULL DEFAULT '', - `s3_endpoint` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'S3 endpoint URL (blank = AWS default)', - `s3_region` VARCHAR(50) NOT NULL DEFAULT 'us-east-1', - `s3_access_key` VARCHAR(255) NOT NULL DEFAULT '', - `s3_secret_key` VARCHAR(255) NOT NULL DEFAULT '', - `s3_bucket` VARCHAR(255) NOT NULL DEFAULT '', - `s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups', `remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload', `encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)', `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'MokoRestore mode: 0=none, 1=wrapped, standalone', diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql new file mode 100644 index 0000000..0551d9d --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.25.sql @@ -0,0 +1,27 @@ +ALTER TABLE `#__mokosuitebackup_profiles` + DROP COLUMN IF EXISTS `remote_storage`, + DROP COLUMN IF EXISTS `ftp_host`, + DROP COLUMN IF EXISTS `ftp_port`, + DROP COLUMN IF EXISTS `ftp_username`, + DROP COLUMN IF EXISTS `ftp_password`, + DROP COLUMN IF EXISTS `ftp_path`, + DROP COLUMN IF EXISTS `ftp_passive`, + DROP COLUMN IF EXISTS `ftp_ssl`, + DROP COLUMN IF EXISTS `sftp_host`, + DROP COLUMN IF EXISTS `sftp_port`, + DROP COLUMN IF EXISTS `sftp_username`, + DROP COLUMN IF EXISTS `sftp_auth_type`, + DROP COLUMN IF EXISTS `sftp_password`, + DROP COLUMN IF EXISTS `sftp_key_data`, + DROP COLUMN IF EXISTS `sftp_passphrase`, + DROP COLUMN IF EXISTS `sftp_path`, + DROP COLUMN IF EXISTS `gdrive_client_id`, + DROP COLUMN IF EXISTS `gdrive_client_secret`, + DROP COLUMN IF EXISTS `gdrive_refresh_token`, + DROP COLUMN IF EXISTS `gdrive_folder_id`, + DROP COLUMN IF EXISTS `s3_endpoint`, + DROP COLUMN IF EXISTS `s3_region`, + DROP COLUMN IF EXISTS `s3_access_key`, + DROP COLUMN IF EXISTS `s3_secret_key`, + DROP COLUMN IF EXISTS `s3_bucket`, + DROP COLUMN IF EXISTS `s3_path`; diff --git a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php index 04b8f33..b0a8c92 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php @@ -321,48 +321,6 @@ class BackupEngine @unlink($archivePath); $this->log('Local copy removed (remote_keep_local = off)'); } - } else { - /* Backward-compat: fall back to legacy single-remote column */ - $remoteStorage = $profile->remote_storage ?? 'none'; - - if ($remoteStorage !== 'none') { - try { - $this->log('Starting remote upload (' . $remoteStorage . ')...'); - $uploader = $this->createUploader($remoteStorage, $profile); - $uploadResult = $uploader->upload($archivePath, $archiveName); - - if ($uploadResult['success']) { - $remoteFilename = $uploadResult['remote_path'] ?? $archiveName; - $this->log('Remote upload complete: ' . $uploadResult['message']); - - if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) { - $restoreBasename = basename($restoreScriptPath); - $this->log('Uploading standalone ' . $restoreBasename . '...'); - $restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename); - - if ($restoreUpload['success']) { - $this->log('Standalone ' . $restoreBasename . ' uploaded'); - } else { - $this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']); - } - } - - // Delete local copy if configured - if (empty($profile->remote_keep_local) && is_file($archivePath)) { - @unlink($archivePath); - $this->log('Local copy removed (remote_keep_local = off)'); - } - } else { - $uploadFailed = true; - $this->log('WARNING: Remote upload failed: ' . $uploadResult['message']); - $this->log('Local backup is preserved.'); - } - } catch (\Throwable $e) { - $uploadFailed = true; - $this->log('WARNING: Remote upload threw an exception: ' . $e->getMessage()); - $this->log('Local backup is preserved.'); - } - } } // Write log file alongside the archive @@ -519,23 +477,7 @@ class BackupEngine } /** - * Create the appropriate remote uploader based on the storage type. - * Legacy method — used by backward-compat fallback when remotes table - * does not exist. - */ - private function createUploader(string $type, object $profile): RemoteUploaderInterface - { - 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), - }; - } - - /** - * Create a remote uploader from JSON params (multi-remote destinations). + * Create a remote uploader from JSON params. * * Builds a fake profile-like object from the params array so the existing * uploader constructors work without modification. @@ -569,31 +511,18 @@ class BackupEngine /** * Load enabled remote destinations for a profile from the remotes table. - * - * Returns an empty array when the table does not exist (pre-migration) - * so the caller can fall back to the legacy single-remote column. - * - * @param object $db Database driver - * @param int $profileId Profile ID - * - * @return object[] Array of remote destination rows */ private function loadRemoteDestinations(object $db, int $profileId): array { - try { - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_remotes')) - ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC'); - $db->setQuery($query); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); - return $db->loadObjectList() ?: []; - } catch (\Throwable $e) { - // Table does not exist yet (pre-migration) — fall back to legacy - return []; - } + return $db->loadObjectList() ?: []; } /** diff --git a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php index 664a3e8..6ccbd90 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php +++ b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php @@ -77,7 +77,7 @@ class PreflightCheck $this->checkDiskSpace($profile, $db); $this->checkRunningBackup($profile, $db); $this->checkExcludedTables($profile, $db); - $this->checkRemoteCredentials($profile); + $this->checkRemoteCredentials($profile, $db); return $this->result(); } @@ -102,12 +102,8 @@ class PreflightCheck } } - // curl is only needed for remote upload and ntfy notifications - $needsCurl = ($profile->remote_storage ?? 'none') !== 'none' - || !empty($profile->ntfy_topic); - - if ($needsCurl && !extension_loaded('curl')) { - $this->warnings[] = 'ext-curl is not loaded — remote upload and ntfy notifications will not work'; + if (!empty($profile->ntfy_topic) && !extension_loaded('curl')) { + $this->warnings[] = 'ext-curl is not loaded — ntfy notifications will not work'; } } @@ -280,65 +276,76 @@ class PreflightCheck } /** - * Check that remote storage credentials are minimally configured. + * Check that remote destination credentials are minimally configured. * Does not test the actual connection (too slow for preflight). */ - private function checkRemoteCredentials(object $profile): void + private function checkRemoteCredentials(object $profile, object $db): void { - $remote = $profile->remote_storage ?? 'none'; + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) + ->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + $remotes = $db->loadObjectList(); - if ($remote === 'none') { + if (empty($remotes)) { return; } - switch ($remote) { - case 'ftp': - if (empty($profile->ftp_host)) { - $this->warnings[] = 'FTP host is not configured — remote upload will fail'; - } + foreach ($remotes as $remote) { + $params = json_decode($remote->params, true) ?: []; + $label = $remote->title ?: ('Remote #' . $remote->id); - if (empty($profile->ftp_username)) { - $this->warnings[] = 'FTP username is not configured — remote upload will fail'; - } + switch ($remote->type) { + case 'ftp': + if (empty($params['host'])) { + $this->warnings[] = $label . ': FTP host is not configured — upload will fail'; + } - break; + if (empty($params['username'])) { + $this->warnings[] = $label . ': FTP username is not configured — upload will fail'; + } - case 's3': - if (empty($profile->s3_bucket)) { - $this->warnings[] = 'S3 bucket is not configured — remote upload will fail'; - } + break; - if (empty($profile->s3_access_key) || empty($profile->s3_secret_key)) { - $this->warnings[] = 'S3 credentials are not configured — remote upload will fail'; - } + case 's3': + if (empty($params['bucket'])) { + $this->warnings[] = $label . ': S3 bucket is not configured — upload will fail'; + } - break; + if (empty($params['access_key']) || empty($params['secret_key'])) { + $this->warnings[] = $label . ': S3 credentials are not configured — upload will fail'; + } - case 'sftp': - if (empty($profile->sftp_host)) { - $this->warnings[] = 'SFTP host is not configured — remote upload will fail'; - } + break; - if (empty($profile->sftp_username)) { - $this->warnings[] = 'SFTP username is not configured — remote upload will fail'; - } + case 'sftp': + if (empty($params['host'])) { + $this->warnings[] = $label . ': SFTP host is not configured — 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'; - } + if (empty($params['username'])) { + $this->warnings[] = $label . ': SFTP username is not configured — upload will fail'; + } - break; + if (empty($params['key_data']) && empty($params['password'])) { + $this->warnings[] = $label . ': SFTP requires either a private key or password — upload will fail'; + } - 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'; - } + break; - if (empty($profile->gdrive_refresh_token)) { - $this->warnings[] = 'Google Drive refresh token is missing — remote upload will fail'; - } + case 'google_drive': + if (empty($params['client_id']) || empty($params['client_secret'])) { + $this->warnings[] = $label . ': Google Drive OAuth credentials are not configured — upload will fail'; + } - break; + if (empty($params['refresh_token'])) { + $this->warnings[] = $label . ': Google Drive refresh token is missing — upload will fail'; + } + + break; + } } } diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php index ee640e7..467b616 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedBackupEngine.php @@ -69,7 +69,6 @@ class SteppedBackupEngine $session->excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? ''); $session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? ''); $session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER; - $session->remoteStorage = $profile->remote_storage ?? 'none'; $session->includeMokoRestore = $profile->include_mokorestore ?? '0'; $session->restoreScriptName = $profile->restore_script_name ?? 'restore.php'; $session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true); @@ -153,15 +152,8 @@ class SteppedBackupEngine $totalSteps += 1; // finalize step - // Determine upload step count: one step per remote destination, - // or one step for legacy single-remote, or zero if no remotes. $remoteCount = count($session->remoteDestinations); - - if ($remoteCount > 0) { - $totalSteps += $remoteCount; - } elseif ($session->remoteStorage !== 'none') { - $totalSteps += 1; - } + $totalSteps += $remoteCount; $session->totalSteps = $totalSteps; $session->currentStep = 1; @@ -421,11 +413,7 @@ class SteppedBackupEngine $session->currentStep++; - // Determine next phase: multi-remote, legacy single-remote, or complete - $hasMultiRemote = !empty($session->remoteDestinations); - $hasLegacyRemote = $session->remoteStorage !== 'none'; - - if ($hasMultiRemote || $hasLegacyRemote) { + if (!empty($session->remoteDestinations)) { $session->phase = 'upload'; } else { $session->phase = 'complete'; @@ -440,11 +428,7 @@ class SteppedBackupEngine } /** - * Upload phase: send archive to remote storage. - * - * When multi-remote destinations are configured, each call uploads to - * one destination (one step per remote). When only the legacy - * single-remote column is set, uploads in a single step. + * Upload phase: send archive to one remote destination per call. */ private function stepUpload(SteppedSession $session): void { @@ -452,133 +436,65 @@ class SteppedBackupEngine $remoteFilename = ''; $uploadFailed = false; - if (!empty($session->remoteDestinations)) { - // ── Multi-remote path ────────────────────────────────── - $index = $session->remoteIndex; + $index = $session->remoteIndex; - if ($index >= count($session->remoteDestinations)) { - // All remotes processed — move to complete - $session->phase = 'complete'; - $session->statusMessage = 'All remote uploads finished'; - $this->completeRecord($session); + if ($index >= count($session->remoteDestinations)) { + $session->phase = 'complete'; + $session->statusMessage = 'All remote uploads finished'; + $this->completeRecord($session); - return; - } + return; + } - $remote = (object) $session->remoteDestinations[$index]; + $remote = (object) $session->remoteDestinations[$index]; - try { - $title = $remote->title ?? ('Remote #' . ($index + 1)); - $type = $remote->type ?? 'unknown'; - $params = json_decode($remote->params ?? '{}', true) ?: []; + try { + $title = $remote->title ?? ('Remote #' . ($index + 1)); + $type = $remote->type ?? 'unknown'; + $params = json_decode($remote->params ?? '{}', true) ?: []; - $session->log('Uploading to: ' . $title . ' (' . $type . ')...'); - $uploader = $this->createUploaderFromParams($type, $params); - $result = $uploader->upload($session->archivePath, $session->archiveName); + $session->log('Uploading to: ' . $title . ' (' . $type . ')...'); + $uploader = $this->createUploaderFromParams($type, $params); + $result = $uploader->upload($session->archivePath, $session->archiveName); - if ($result['success']) { - $remoteFilename = $result['remote_path'] ?? $session->archiveName; - $session->log(' Upload complete: ' . $result['message']); + if ($result['success']) { + $remoteFilename = $result['remote_path'] ?? $session->archiveName; + $session->log(' Upload complete: ' . $result['message']); - if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { - $uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath)); - } - } else { - $uploadFailed = true; - $session->log(' WARNING: Upload failed: ' . $result['message']); + if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { + $uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath)); } - } catch (\Throwable $e) { + } else { $uploadFailed = true; - $session->log(' WARNING: Upload exception: ' . $e->getMessage()); + $session->log(' WARNING: Upload failed: ' . $result['message']); + } + } catch (\Throwable $e) { + $uploadFailed = true; + $session->log(' WARNING: Upload exception: ' . $e->getMessage()); + } + + $session->remoteIndex++; + $session->currentStep++; + + $remaining = count($session->remoteDestinations) - $session->remoteIndex; + $session->statusMessage = 'Uploaded to ' . ($remote->title ?? 'remote') . ($remaining > 0 ? ' (' . $remaining . ' remaining)' : ''); + + if ($session->remoteIndex >= count($session->remoteDestinations)) { + if (!$uploadFailed && !$session->remoteKeepLocal && is_file($session->archivePath)) { + @unlink($session->archivePath); + $session->log('Local copy removed (remote_keep_local = off)'); } - $session->remoteIndex++; - $session->currentStep++; - - $remaining = count($session->remoteDestinations) - $session->remoteIndex; - $session->statusMessage = 'Uploaded to ' . ($remote->title ?? 'remote') . ($remaining > 0 ? ' (' . $remaining . ' remaining)' : ''); - - if ($session->remoteIndex >= count($session->remoteDestinations)) { - // All remotes done — delete local if configured and no failures - if (!$uploadFailed && !$session->remoteKeepLocal && is_file($session->archivePath)) { - @unlink($session->archivePath); - $session->log('Local copy removed (remote_keep_local = off)'); - } - - // Update record with remote filename - $update = (object) [ - 'id' => $session->recordId, - 'remote_filename' => $remoteFilename, - 'filesexist' => is_file($session->archivePath) ? 1 : 0, - ]; - $db->updateObject('#__mokosuitebackup_records', $update, 'id'); - - $session->phase = 'complete'; - $session->statusMessage = $uploadFailed - ? 'Backup complete (some remote uploads failed — local archive preserved)' - : 'Backup complete'; - $this->completeRecord($session, $uploadFailed); - } - } else { - // ── Legacy single-remote fallback ────────────────────── - try { - // Reload profile for remote settings - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_profiles')) - ->where($db->quoteName('id') . ' = ' . $session->profileId); - $db->setQuery($query); - $profile = $db->loadObject(); - - $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), - }; - - $session->log('Starting remote upload (' . $session->remoteStorage . ')...'); - $result = $uploader->upload($session->archivePath, $session->archiveName); - - if ($result['success']) { - $remoteFilename = $result['remote_path'] ?? $session->archiveName; - $session->log('Remote upload complete: ' . $result['message']); - - if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) { - $restoreBasename = basename($session->restoreScriptPath); - $session->log('Uploading standalone ' . $restoreBasename . '...'); - $uploader->upload($session->restoreScriptPath, $restoreBasename); - } - - if (!$session->remoteKeepLocal && is_file($session->archivePath)) { - @unlink($session->archivePath); - $session->log('Local copy removed'); - } - } else { - $uploadFailed = true; - $session->log('WARNING: Remote upload failed: ' . $result['message']); - $session->log('Local backup is preserved.'); - } - } catch (\Throwable $e) { - $uploadFailed = true; - $session->log('WARNING: Remote upload threw an exception: ' . $e->getMessage()); - $session->log('Local backup is preserved.'); - } - - // Update record with remote filename $update = (object) [ 'id' => $session->recordId, 'remote_filename' => $remoteFilename, 'filesexist' => is_file($session->archivePath) ? 1 : 0, ]; - $db->updateObject('#__mokosuitebackup_records', $update, 'id'); - $session->currentStep++; $session->phase = 'complete'; $session->statusMessage = $uploadFailed - ? 'Backup complete (remote upload failed — local archive preserved)' + ? 'Backup complete (some remote uploads failed — local archive preserved)' : 'Backup complete'; $this->completeRecord($session, $uploadFailed); } @@ -859,21 +775,15 @@ class SteppedBackupEngine */ private function loadRemoteDestinations(object $db, int $profileId): array { - try { - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__mokosuitebackup_remotes')) - ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('ordering') . ' ASC'); - $db->setQuery($query); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokosuitebackup_remotes')) + ->where($db->quoteName('profile_id') . ' = ' . (int) $profileId) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); - // Use loadAssocList so the data survives JSON serialization in SteppedSession - return $db->loadAssocList() ?: []; - } catch (\Throwable $e) { - // Table does not exist yet (pre-migration) — fall back to legacy - return []; - } + return $db->loadAssocList() ?: []; } /** diff --git a/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php b/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php index c153c52..3a103b3 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php +++ b/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php @@ -50,7 +50,6 @@ class SteppedSession public array $excludeDirs = []; public array $excludeFiles = []; public array $excludeTables = []; - public string $remoteStorage = 'none'; public string $includeMokoRestore = '0'; public string $restoreScriptName = 'restore.php'; public string $restoreScriptPath = ''; diff --git a/source/packages/com_mokosuitebackup/tmpl/profile/edit.php b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php index 1f6e636..779a212 100644 --- a/source/packages/com_mokosuitebackup/tmpl/profile/edit.php +++ b/source/packages/com_mokosuitebackup/tmpl/profile/edit.php @@ -65,7 +65,6 @@ $token = Session::getFormToken();
-
@@ -97,20 +96,13 @@ $token = Session::getFormToken();

-
+ +
+ +
- -
- - form->renderFieldset('remote'); ?> - form->renderFieldset('ftp'); ?> - form->renderFieldset('google_drive'); ?> - form->renderFieldset('s3'); ?> -
+ form->renderFieldset('remote'); ?>
@@ -280,9 +272,8 @@ document.addEventListener('DOMContentLoaded', function() { const tbody = document.getElementById('remoteDestBody'); const emptyMsg = document.getElementById('remoteDestEmpty'); const loadingTr = document.getElementById('remoteDestLoading'); - const legacy = document.getElementById('legacyRemoteFields'); - const legacyNote = document.getElementById('legacyRemoteNote'); - const modal = new bootstrap.Modal(document.getElementById('remoteModal')); + const modalEl = document.getElementById('remoteModal'); + const modal = bootstrap.Modal.getOrCreateInstance(modalEl); // Type badge colours const typeBadge = {sftp: 'bg-primary', s3: 'bg-warning text-dark', google_drive: 'bg-success'}; @@ -336,14 +327,10 @@ document.addEventListener('DOMContentLoaded', function() { if (!remotesData.length) { emptyMsg.style.display = ''; - legacy.style.display = ''; - legacyNote.style.display = 'none'; return; } emptyMsg.style.display = 'none'; - legacy.style.display = 'none'; - legacyNote.style.display = 'block'; remotesData.forEach(function(item) { const tr = document.createElement('tr'); @@ -506,8 +493,8 @@ document.addEventListener('DOMContentLoaded', function() { const fields = configFields[item.type] || []; fields.forEach(function(f) { const el = document.getElementById('remoteCfg_' + prefix + f); - if (el && item.config && item.config[f] !== undefined) { - el.value = item.config[f]; + if (el && item.params && item.params[f] !== undefined) { + el.value = item.params[f]; } }); } diff --git a/source/script.php b/source/script.php index 67668df..b3d47fc 100644 --- a/source/script.php +++ b/source/script.php @@ -665,41 +665,23 @@ class Pkg_MokoSuiteBackupInstallerScript { try { - $db = Factory::getDbo(); - $db->setQuery( - $db->getQuery(true) - ->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')]) - ->from($db->quoteName('#__update_sites')) - ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteBackup%') . ')') - ->setLimit(1) - ); - $site = $db->loadObject(); - - if ($site) - { - $eq = (string) ($site->extra_query ?? ''); - if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } } - $editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id; - } - else - { - $editUrl = 'index.php?option=com_installer&view=updatesites'; - } - Factory::getApplication()->enqueueMessage( - 'Moko Consulting License Key Required — ' - . 'No download key is configured. Updates will not be available until a valid license key is entered. ' - . 'Enter License Key', - 'warning' + '

MokoSuiteBackup installed successfully!

', + 'info' + ); + + // Show post-install license key prompt + Factory::getApplication()->enqueueMessage( + 'Moko Consulting License Key Required
' + . 'A download key (DLID) is required to receive updates. ' + . 'Enter your key in the Update Sites manager ' + . 'or contact Moko Consulting Support to obtain one.', + 'warning' ); } - catch (\Exception $e) { - error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage()); - Factory::getApplication()->enqueueMessage( - 'MokoSuiteBackup could not verify your license key status. ' - . 'Please check System → Update Sites to ensure a valid license key is configured.', - 'warning' - ); + catch (\Exception $e) + { + error_log('MokoSuiteBackup: License key prompt failed: ' . $e->getMessage()); } } }