a13f7ca6a6
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Update all references in Makefile, manifest.xml, .gitignore, and CI workflows (ci-joomla, pr-check, repo-health) to use source/ as the primary directory with src/ as a fallback for compatibility. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
4.1 KiB
PHP
165 lines
4.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoJoomBackup
|
|
* @subpackage com_mokobackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
class FtpUploader implements RemoteUploaderInterface
|
|
{
|
|
private string $host;
|
|
private int $port;
|
|
private string $username;
|
|
private string $password;
|
|
private string $remotePath;
|
|
private bool $passive;
|
|
private bool $ssl;
|
|
|
|
public function __construct(object $profile)
|
|
{
|
|
$this->host = $profile->ftp_host ?? '';
|
|
$this->port = (int) ($profile->ftp_port ?? 21);
|
|
$this->username = $profile->ftp_username ?? '';
|
|
$this->password = $profile->ftp_password ?? '';
|
|
$this->remotePath = rtrim($profile->ftp_path ?? '/backups', '/');
|
|
$this->passive = (bool) ($profile->ftp_passive ?? true);
|
|
$this->ssl = (bool) ($profile->ftp_ssl ?? false);
|
|
}
|
|
|
|
public function upload(string $localPath, string $remoteName): array
|
|
{
|
|
if (!extension_loaded('ftp')) {
|
|
return ['success' => false, 'message' => 'PHP ext-ftp is required for FTP uploads. Enable it in php.ini.'];
|
|
}
|
|
|
|
if (empty($this->host)) {
|
|
return ['success' => false, 'message' => 'FTP host is not configured'];
|
|
}
|
|
|
|
$conn = $this->connect();
|
|
|
|
if (!$conn) {
|
|
return ['success' => false, 'message' => 'Failed to connect to FTP server ' . $this->host . ':' . $this->port];
|
|
}
|
|
|
|
try {
|
|
if (!@ftp_login($conn, $this->username, $this->password)) {
|
|
throw new \RuntimeException('FTP login failed for user: ' . $this->username);
|
|
}
|
|
|
|
if ($this->passive) {
|
|
ftp_pasv($conn, true);
|
|
}
|
|
|
|
// Ensure remote directory exists (create recursively)
|
|
$this->ensureRemoteDir($conn, $this->remotePath);
|
|
|
|
$remoteFile = $this->remotePath . '/' . $remoteName;
|
|
|
|
if (!@ftp_put($conn, $remoteFile, $localPath, FTP_BINARY)) {
|
|
throw new \RuntimeException('Failed to upload file to: ' . $remoteFile);
|
|
}
|
|
|
|
// Verify upload by checking remote file size
|
|
$remoteSize = @ftp_size($conn, $remoteFile);
|
|
$localSize = filesize($localPath);
|
|
|
|
if ($remoteSize >= 0 && $remoteSize !== $localSize) {
|
|
throw new \RuntimeException(
|
|
'Size mismatch: local=' . $localSize . ' remote=' . $remoteSize
|
|
);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Uploaded to FTP: ' . $remoteFile,
|
|
'remote_path' => $remoteFile,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
return ['success' => false, 'message' => 'FTP upload failed: ' . $e->getMessage()];
|
|
} finally {
|
|
@ftp_close($conn);
|
|
}
|
|
}
|
|
|
|
public function testConnection(): array
|
|
{
|
|
if (empty($this->host)) {
|
|
return ['success' => false, 'message' => 'FTP host is not configured'];
|
|
}
|
|
|
|
$conn = $this->connect();
|
|
|
|
if (!$conn) {
|
|
return ['success' => false, 'message' => 'Cannot connect to ' . $this->host . ':' . $this->port];
|
|
}
|
|
|
|
try {
|
|
if (!@ftp_login($conn, $this->username, $this->password)) {
|
|
return ['success' => false, 'message' => 'Login failed for user: ' . $this->username];
|
|
}
|
|
|
|
if ($this->passive) {
|
|
ftp_pasv($conn, true);
|
|
}
|
|
|
|
$pwd = @ftp_pwd($conn);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Connected to ' . $this->host . ' (cwd: ' . $pwd . ')',
|
|
];
|
|
} finally {
|
|
@ftp_close($conn);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return resource|false
|
|
*/
|
|
private function connect()
|
|
{
|
|
$timeout = 30;
|
|
|
|
if ($this->ssl) {
|
|
return @ftp_ssl_connect($this->host, $this->port, $timeout);
|
|
}
|
|
|
|
return @ftp_connect($this->host, $this->port, $timeout);
|
|
}
|
|
|
|
/**
|
|
* Recursively create remote directories.
|
|
*
|
|
* @param resource $conn FTP connection
|
|
* @param string $path Remote directory path
|
|
*/
|
|
private function ensureRemoteDir($conn, string $path): void
|
|
{
|
|
$parts = explode('/', trim($path, '/'));
|
|
$current = '';
|
|
|
|
foreach ($parts as $part) {
|
|
$current .= '/' . $part;
|
|
|
|
if (@ftp_chdir($conn, $current)) {
|
|
continue;
|
|
}
|
|
|
|
if (!@ftp_mkdir($conn, $current)) {
|
|
throw new \RuntimeException('Cannot create remote directory: ' . $current);
|
|
}
|
|
}
|
|
|
|
// Return to root
|
|
@ftp_chdir($conn, '/');
|
|
}
|
|
}
|