پلاگین سیستم فایل – FTP
- محمد علایی
- منتشر شده در
- زمان خواندن 15 دقیقه
این پلاگین بر اساس پلاگین پایهی filesystem ساخته شده تا نشان دهد چگونه میتوان جوملا را قادر ساخت از یک سیستم فایل رسانهای که در فایلاستور محلی نیست، استفاده کند. در اینجا ما یک پلاگین توسعه میدهیم که اجازه میدهد فایلاستور رسانه از طریق FTP قابل دسترسی باشد.
نوشتن پلاگین Filesystem FTP
برای نوشتن یک پلاگین FTP Filesystem به دو کلاس نیاز دارید:
- کلاس Provider که اینترفیس Joomla\Component\Media\Administrator\Provider\ProviderInterface را در مسیر administrator/components/com_media/src/Provider/ProviderInterface.php پیادهسازی میکند. این قسمت ساده است و شما میتوانید روش پلاگین Local Filesystem را در plugins/filesystem/local/src/Extension/Local.php کپی کنید. کلاس اصلی پلاگین (افزونه) به عنوان کلاس Provider نیز عمل میکند.
- کلاس Adapter که اینترفیس Joomla\Component\Media\Administrator\Adapter\AdapterInterface را در مسیر administrator/components/com_media/src/Adapter/AdapterInterface.php پیادهسازی میکند. عمدهی کار در اینجا است، چون باید انواع عملیات فایل را به فراخوانیهای FTP نگاشت کنید. مجموعه توابع PHP مربوط به FTP در FTP Functions فهرست شدهاند.
آزمایش
برای آزمایش بخشهای FTP باید یک سرور FTP روی ماشین محلیتان نصب کنید. من برای تست کد پلاگین filesystem از Filezilla استفاده کردم، اما توجه کنید ممکن است تفاوتهایی در رابط کاربری بین پیادهسازیهای مختلف سرور FTP وجود داشته باشد. بهخصوص، تابع PHP به نام ftp_mlsd ممکن است موجود نباشد و شما برای مدیریت لیست فایلها به جای آن باید از ftp_nlist یا ftp_rawlist استفاده کنید. در Filezilla یک کاربر آزمایشی و یک نقطه اتصال (mount point) تعریف کردم که مسیر /shared را به یک فولدر روی کامپیوترم مپ میکند. این جزییات باید توسط پلاگین شناخته شود، بنابراین به عنوان پارامترهای تنظیمات پلاگین در فایل XML manifest تعریف میشوند:
- host: من localhost را استفاده کردم
- username: نام کاربری کاربر روی سرور FTP
- password: رمز عبور کاربر روی سرور FTP
- ftproot: مسیر مجازی نقطه اتصال؛ من "shared" را استفاده کردم
برای تست، نوشتن یک برنامه کوچکی به زبان PHP مفید بود که نتایج فراخوانی توابع مختلف FTP را نشان میداد.
اتصال FTP
باید اتصال FTP را در هر درخواست HTTP باز و بسته کنید، چون اگر تلاش کنید اتصال را بین درخواستهای HTTP باز نگه دارید، ممکن است اتصال تایماوت شود. در این کد پلاگین filesystem، باز کردن اتصال و لاگین کاربر در سازنده (constructor) انجام میشود و بسته شدن اتصال در مخرب (destructor).
public function __construct(string $ftp_server, string $ftp_username, string $ftp_password, string $ftp_root, string $url_root)
{
...
if (!$this->ftp_connection = @ftp_connect($this->ftp_server)) {
...
}
if (!@ftp_login($this->ftp_connection, $this->ftp_username, $this->ftp_password)) {
...
}
}
public function __destruct()
{
if ($this->ftp_connection) {
@ftp_close($this->ftp_connection);
}
}
آدرسهای URL
اگر بخواهید رسانههای خود را در وبسایت بخش جلویی (فرانتاند) نمایش دهید، باید URLهایی برای دسترسی بازدیدکنندگان فراهم کنید که این کار را از طریق تابع `getUrl()` در آداپتر خود انجام میدهید. شما دو گزینه دارید:
1. اگر وبسرور روی همان سرور FTP شما باشد و دایرکتوری FTP از طریق وبسرور قابل دسترسی باشد، کافی است URL مربوط به همین وبسرور را بسازید.
2. در غیر این صورت، باید فایل را از سرور FTP دانلود کرده و در یک دایرکتوری محلی ذخیره کنید و URL مربوط به فایل محلی را بسازید.
در کد پلاگین فایلسیستم، هر دو گزینه برنامهنویسی شدهاند و میتوانید با تنظیم پارامتر پلاگین `"urlroot"` مشخص کنید کدام گزینه استفاده شود:
- اگر مقدار `"urlroot"` را بدهید، کد URL را از این مقدار و آرگومان `$path` میسازد.
- اگر مقدار ندهید، کد فایل را دانلود کرده و در دایرکتوری `/tmp` جوملا ذخیره میکند، با استفاده از هش نام فایل برای نام فایل موقتی، و URL مربوطه را تولید میکند. (هش xxh3 استفاده شده نیازمند حداقل PHP 8.1 است، اما میتوانید آن را تغییر دهید).
public function getUrl(string $path): string
{
...
if ($this->url_root) {
return $this->url_root . $path;
} else {
$hash = hash("xxh3", $path);
$local_filename = JPATH_ROOT . '/tmp/' . $hash . '.tmp';
if (file_exists($local_filename)) {
return Uri::root() . 'tmp/' . $hash . '.tmp';
} else {
if (!@ftp_get($this->ftp_connection, $local_filename, $this->ftp_root . $path)) {
...
}
return Uri::root() . 'tmp/' . $hash . '.tmp';
}
}
}
برای جلوگیری از دانلود مکرر فایل، کد چک میکند اگر فایل قبلاً در دایرکتوری `/tmp` موجود باشد. البته این در محیط تولید (Production) کارایی ندارد چون ممکن است محتوای فایل روی سرور FTP تغییر کند، اما میتوانید این روش را برای ایجاد قابلیت کش بهتر کنید.
کد منبع پلاگین
میتوانید کد منبع زیر را در یک دایرکتوری به نام `plg_filesystem_ftp` کپی کنید، یا پلاگین کامل را از لینک دانلود پلاگین فایلسیستم FTP دریافت کنید.
پس از نصب، فراموش نکنید که پلاگین را فعال کنید! همچنین باید سرور FTP محلی خود را اجرا کرده و پلاگین را با مشخصات سرور FTP خود پیکربندی کنید.
فایل منیفست
`plg_filesystem_ftp/ftp.xml`
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="filesystem" method="upgrade">
<name>plg_filesystem_ftp</name>
<author>من</author>
<creationDate>امروز</creationDate>
<version>1.0.0</version>
<description>سیستمفایل FTP من</description>
<namespace path="src">My\Plugin\Filesystem\Ftp</namespace>
<files>
<folder plugin="ftp">services</folder>
<folder>src</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="host"
type="text"
label="میزبان سرور FTP"
default=""
>
</field>
<field
name="username"
type="text"
label="نام کاربری FTP"
default=""
>
</field>
<field
name="password"
type="text"
label="رمز عبور FTP"
default=""
>
</field>
<field
name="ftproot"
type="text"
label="نقطه اتصال ریشه FTP"
default=""
>
</field>
<field
name="urlroot"
type="text"
label="آدرس پایه URL دایرکتوری سرور FTP"
default=""
>
</field>
</fieldset>
</fields>
</config>
</extension>
فایل ارائهدهنده سرویس
این کد الگو (boilerplate) برای نمونهسازی پلاگین از طریق کانتینر تزریق وابستگی جوملا است.
`plg_filesystem_ftp/services/provider.php`
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use My\Plugin\Filesystem\Ftp\Extension\Ftp;
return new class () implements ServiceProviderInterface {
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new Ftp(
$dispatcher,
(array) PluginHelper::getPlugin('filesystem', 'ftp')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
کلاس پلاگین / ارائهدهنده
این کلاس برگرفته شده از کلاس معادل آن در پلاگین فایلسیستم محلی جوملا است.
`plg_filesystem_ftp/src/Extension/Ftp.php`
<?php
namespace My\Plugin\Filesystem\Ftp\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Media\Administrator\Event\MediaProviderEvent;
use Joomla\Component\Media\Administrator\Provider\ProviderInterface;
use Joomla\Event\DispatcherInterface;
use My\Plugin\Filesystem\Ftp\Adapter\FtpAdapter;
use Joomla\CMS\Factory;
\defined('_JEXEC') or die;
final class Ftp extends CMSPlugin implements ProviderInterface
{
public static function getSubscribedEvents(): array {
return [
'onSetupProviders' => 'onSetupProviders',
];
}
/**
* پیکربندی ارائهدهندهها برای آداپتر FTP
*
* @param MediaProviderEvent $event رویداد مربوط به ProviderManager
*
* @return void
*
* @since 4.0.0
*/
public function onSetupProviders(MediaProviderEvent $event)
{
$event->getProviderManager()->registerProvider($this);
}
/**
* بازگرداندن شناسه ارائهدهنده
*
* @return string
*
* @since 4.0.0
*/
public function getID()
{
return $this->_name; // از فیلد "element" رکورد پلاگین در جدول افزونهها
}
/**
* بازگرداندن نام نمایشی ارائهدهنده
*
* @return string
*
* @since 4.0.0
*/
public function getDisplayName()
{
return 'FTP از راه دور';
}
/**
* بازگرداندن آرایهای از آداپترها
*
* @return \Joomla\Component\Media\Administrator\Adapter\AdapterInterface[]
*
* @since 4.0.0
*/
public function getAdapters()
{
$adapters = [];
$ftp_server = $this->params->get('server', '');
$ftp_username = $this->params->get('username', '');
$ftp_password = $this->params->get('password', '');
$ftp_root = $this->params->get('ftproot', '');
$url_root = $this->params->get('urlroot', '');
$adapter = new FtpAdapter($ftp_server, $ftp_username, $ftp_password, $ftp_root, $url_root);
$adapters[$adapter->getAdapterName()] = $adapter;
return $adapters;
}
}
کلاس آداپتور (Adapter)
اینجا جایی است که تمام کارها انجام میشود! به وضوح بین کد getFile و getFiles همپوشانی وجود دارد، اما کد به این صورت باقی مانده است تا درک عملکرد هر تابع آسانتر شود.
plg_filesystem_ftp/src/Adapter/FtpAdapter.php
<?php
namespace My\Plugin\Filesystem\Ftp\Adapter;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\String\PunycodeHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Media\Administrator\Adapter\AdapterInterface;
use Joomla\Component\Media\Administrator\Exception\FileNotFoundException;
use Joomla\CMS\Log\Log;
\defined('_JEXEC') or die;
class FtpAdapter implements AdapterInterface
{
// Incomplete mapping of file extension to mime type
static $mapper = array(
'.avi' => 'video/avi',
'.bmp' => 'image/bmp',
'.gif' => 'image/gif',
'.jpeg' => 'image/jpeg',
'.jpg' => 'image/jpeg',
'.mp3' => 'audio/mpeg',
'.mp4' => 'video/mp4',
'.mpeg' => 'video/mpeg',
'.pdf' => 'application/pdf',
'.png' => 'image/png',
);
// Configuration from the plugin parameters
private $ftp_server = "";
private $ftp_username = "";
private $ftp_password = "";
private $ftp_root = "";
private $url_root = "";
// ftp connection
private $ftp_connection = null;
public function __construct(string $ftp_server, string $ftp_username, string $ftp_password, string $ftp_root, string $url_root)
{
$this->ftp_server = $ftp_server;
$this->ftp_username = $ftp_username;
$this->ftp_password = $ftp_password;
$this->ftp_root = $ftp_root;
$this->url_root = $url_root;
if (!$this->ftp_connection = @ftp_connect($this->ftp_server)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't connect: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
if (!@ftp_login($this->ftp_connection, $this->ftp_username, $this->ftp_password)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't login: {$message}", Log::ERROR, 'ftp');
@ftp_close($this->ftp_connection);
$this->ftp_connection = null;
throw new \Exception($message);
}
}
public function __destruct()
{
if ($this->ftp_connection) {
@ftp_close($this->ftp_connection);
}
}
/**
* This is the comment from the LocalAdapter interface - but it's not complete!
*
* Returns the requested file or folder. The returned object
* has the following properties available:
* - type: The type can be file or dir
* - name: The name of the file
* - path: The relative path to the root
* - extension: The file extension
* - size: The size of the file
* - create_date: The date created
* - modified_date: The date modified
* - mime_type: The mime type
* - width: The width, when available
* - height: The height, when available
*
* If the path doesn't exist a FileNotFoundException is thrown.
*
* @param string $path The path to the file or folder
*
* @return \stdClass
*
* @since 4.0.0
* @throws \Exception
*/
public function getFile(string $path = '/'): \stdClass
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
// To get the file details we need to run mlsd on the directory
$slash = strrpos($path, '/');
if ($slash === false) {
Log::add("FTP unexpectedly no slash in path {$path}", Log::ERROR, 'ftp');
return [];
}
if ($slash) {
$directory = substr($path, 0, $slash);
$filename = substr($path, $slash + 1);
} else { // it's the top level directory
$directory = "";
$filename = substr($path, 1);
}
if (!$files = ftp_mlsd($this->ftp_connection, $this->ftp_root . $directory)) {
throw new FileNotFoundException();
}
foreach ($files as $file) {
if ($file['name'] == $filename) {
$obj = new \stdClass();
$obj->type = $file['type'];
$obj->name = $file['name'];
$obj->path = $path;
$obj->extension = $file['type'] == 'file' ? File::getExt($obj->name) : '';
$obj->size = $file['type'] == 'file' ? intval($file['size']) : '';
$obj->create_date = $this->convertDate($file['modify']);
$obj->create_date_formatted = $obj->create_date;
$obj->modified_date = $obj->create_date;
$obj->modified_date_formatted = $obj->create_date_formatted;
$obj->mime_type = $file['type'] == 'file' ? $this->extension_mime_mapper(strrchr($file['name'], ".")) : "directory";
$obj->width = 0;
$obj->height = 0;
// Add thumbnail preview when possible. Recomended size: should fit in to 200x200px box.
// Relative or full path.
$obj->thumb_path = Uri::root(true) . '/images/powered_by.png';
// Optional, an actual size of thumbnail to enable lazy loading
$obj->thumb_width = 200;
$obj->thumb_height = 44;
return $obj;
}
}
throw new FileNotFoundException();
}
/**
* Returns the folders and files for the given path. The returned objects
* have the following properties available:
* - type: The type can be file or dir
* - name: The name of the file
* - path: The relative path to the root
* - extension: The file extension
* - size: The size of the file
* - create_date: The date created
* - modified_date: The date modified
* - mime_type: The mime type
* - width: The width, when available
* - height: The height, when available
*
* If the path doesn't exist a FileNotFoundException is thrown.
*
* @param string $path The folder
*
* @return \stdClass[]
*
* @since 4.0.0
* @throws \Exception
*/
public function getFiles(string $path = '/'): array
{
// This can be called with a folder or a file, eg
// $path = '/' is the top level folder
// $path = '/sub' is the folder sub under the top level
// $path = '/fname.png' is a file in the top level folder
// $path = '/sub/fname.jpg' is a file in the sub folder
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
$result = [];
$requestedDirectory = "";
$pathPrefix = "";
if ($path == '/') {
$requestedDirectory = $this->ftp_root;
$pathPrefix = "";
} else {
$slash = strrpos($path, '/');
if ($slash === false) {
Log::add("FTP unexpectedly no slash in path {$path}", Log::ERROR, 'ftp');
return [];
}
$parentDirectory = $this->ftp_root . substr($path, 0, $slash);
$filename = substr($path, $slash + 1);
// run mlsd and try to match on the filename, to determine if it's a file or directory
if (!$files = ftp_mlsd($this->ftp_connection, $parentDirectory)) {
return [];
}
foreach ($files as $file) {
if ($file['name'] == $filename) {
// it's a file, just get the file details and return them
if ($file['type'] == 'file') {
$obj = new \stdClass();
$obj->type = $file['type'];
$obj->name = $file['name'];
$obj->path = $path;
$obj->extension = $file['type'] == 'file' ? File::getExt($obj->name) : '';
$obj->size = $file['type'] == 'file' ? intval($file['size']) : '';
$obj->create_date = $this->convertDate($file['modify']);
$obj->create_date_formatted = $obj->create_date;
$obj->modified_date = $obj->create_date;
$obj->modified_date_formatted = $obj->create_date_formatted;
$obj->mime_type = $file['type'] == 'file' ? $this->extension_mime_mapper(strrchr($file['name'], ".")) : "directory";
$obj->width = 0;
$obj->height = 0;
// Add thumbnail preview when possible. Recomended size: should fit in to 200x200px box.
// Relative or full path.
$obj->thumb_path = Uri::root(true) . '/images/powered_by.png';
// Optional, an actual size of thumbnail to enable lazy loading
$obj->thumb_width = 200;
$obj->thumb_height = 44;
$results[] = $obj;
return $results;
} else {
$requestedDirectory = $this->ftp_root . $path;
$pathPrefix = $path;
break; // it was a directory
}
}
}
}
// need to run mlsd again, this time on the requested directory
if (!$files = ftp_mlsd($this->ftp_connection, $requestedDirectory)) {
return [];
}
foreach ($files as $file) {
$obj = new \stdClass();
$obj->type = $file['type'];
$obj->name = $file['name'];
$obj->path = $pathPrefix . '/' . $file['name'];
$obj->extension = $file['type'] == 'file' ? File::getExt($obj->name) : '';
$obj->size = $file['type'] == 'file' ? intval($file['size']) : '';
$obj->create_date = $this->convertDate($file['modify']);
$obj->create_date_formatted = $obj->create_date;
$obj->modified_date = $obj->create_date;
$obj->modified_date_formatted = $obj->create_date_formatted;
$obj->mime_type = $file['type'] == 'file' ? $this->extension_mime_mapper(strrchr($file['name'], ".")) : "directory";
$obj->width = 0;
$obj->height = 0;
// Add thumbnail preview when possible. Recomended size: should fit in to 200x200px box.
// Relative or full path.
$obj->thumb_path = Uri::root(true) . '/images/powered_by.png';
// Optional, an actual size of thumbnail to enable lazy loading
$obj->thumb_width = 200;
$obj->thumb_height = 44;
$results[] = $obj;
}
return $results;
}
function convertDate($date_string) {
$d = date_parse_from_format("YmdHis\.v", $date_string);
$date_formatted = sprintf("%04d-%02d-%02d %02d:%02d", $d['year'], $d['month'], $d['day'], $d['hour'], $d['minute']);
return $date_formatted;
}
function extension_mime_mapper($extension) {
if (array_key_exists($extension, self::$mapper)) {
return self::$mapper[$extension];
} else {
return 'application/octet-stream';
}
}
/**
* Returns a resource to download the path.
*
* @param string $path The path to download
*
* @return resource
*
* @since 4.0.0
* @throws \Exception
*/
public function getResource(string $path)
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
// write the data to PHP://temp stream
$handle = fopen('php://temp', 'w+');
if (!@ftp_fget($this->ftp_connection, $handle, $this->ftp_root . $path)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't get file {$path}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
rewind($handle);
return $handle;
}
/**
* Creates a folder with the given name in the given path.
*
* It returns the new folder name. This allows the implementation
* classes to normalise the file name.
*
* @param string $name The name
* @param string $path The folder
*
* @return string
*
* @since 4.0.0
* @throws \Exception
*/
public function createFolder(string $name, string $path): string
{
$name = $this->getSafeName($name);
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
$directory = $this->ftp_root . $path . '/' . $name;
if (!@ftp_mkdir($this->ftp_connection, $directory)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP error on mkdir {$directory}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
return $name;
}
/**
* Creates a file with the given name in the given path with the data.
*
* It returns the new file name. This allows the implementation
* classes to normalise the file name.
*
* @param string $name The name
* @param string $path The folder
* @param string $data The data
*
* @return string
*
* @since 4.0.0
* @throws \Exception
*/
public function createFile(string $name, string $path, $data): string
{
$name = $this->getSafeName($name);
$remote_filename = $this->ftp_root . $path . '/' . $name;
// write the data to PHP://temp stream
$handle = fopen('php://temp', 'w+');
fwrite($handle, $data);
rewind($handle);
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
if (!@ftp_fput($this->ftp_connection, $remote_filename, $handle, FTP_BINARY)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't create file {$remote_filename}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
fclose($handle);
return $name;
}
/**
* Updates the file with the given name in the given path with the data.
*
* @param string $name The name
* @param string $path The folder
* @param string $data The data
*
* @return void
*
* @since 4.0.0
* @throws \Exception
*/
public function updateFile(string $name, string $path, $data)
{
$name = $this->getSafeName($name);
$remote_filename = $this->ftp_root . $path . '/' . $name;
// write the data to PHP://temp stream
$handle = fopen('php://temp', 'w+');
fwrite($handle, $data);
rewind($handle);
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
ftp_pasv($this->ftp_connection, true); // may not be necessary
if (!@ftp_fput($this->ftp_connection, $remote_filename, $handle, FTP_BINARY)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't create file {$remote_filename}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
fclose($handle);
return;
}
/**
* Deletes the folder or file of the given path.
*
* @param string $path The path to the file or folder
*
* @return void
*
* @since 4.0.0
* @throws \Exception
*/
public function delete(string $path)
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
// We have to find if this is a file or if it's a directory.
// So we split the directory path from the filename and then call mlsd on the directory
$slash = strrpos($path, '/');
if ($slash === false) {
Log::add("FTP delete: unexpectedly no slash in path {$path}", Log::ERROR, 'ftp');
return [];
}
$directory = substr($path, 0, $slash);
$filename = substr($path, $slash + 1);
if (!$files = ftp_mlsd($this->ftp_connection, $this->ftp_root . $directory)) {
Log::add("Can't delete non-existent file {$path}", Log::ERROR, 'ftp');
return;
}
// Go through the files in the folder looking for a match with a file or directory
foreach ($files as $file) {
if ($file['name'] == $filename) {
if ($file['type'] == 'file') {
if (!$result = @ftp_delete($this->ftp_connection, $this->ftp_root . $path)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("Unable to delete file {$path}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
} else {
if (!$result = @ftp_rmdir($this->ftp_connection, $this->ftp_root . $path)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("Unable to delete directory {$path}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
}
return;
}
}
}
/**
* Copies a file or folder from source to destination.
*
* It returns the new destination path. This allows the implementation
* classes to normalise the file name.
*
* @param string $sourcePath The source path
* @param string $destinationPath The destination path
* @param bool $force Force to overwrite
*
* @return string
*
* @since 4.0.0
* @throws \Exception
*/
public function copy(string $sourcePath, string $destinationPath, bool $force = false): string
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
// copy the data of the source file down into PHP://temp stream
$handle = fopen('php://temp', 'w+');
if (!@ftp_fget($this->ftp_connection, $handle, $this->ftp_root . $sourcePath)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't get file {$sourcePath} for copying: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
rewind($handle);
// copy from the temp stream up to the destination
if (!@ftp_fput($this->ftp_connection, $this->ftp_root . $destinationPath, $handle)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP can't copy to file {$destinationPath}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
fclose($handle);
return $destinationPath;
}
/**
* Moves a file or folder from source to destination.
*
* It returns the new destination path. This allows the implementation
* classes to normalise the file name.
*
* @param string $sourcePath The source path
* @param string $destinationPath The destination path
* @param bool $force Force to overwrite
*
* @return string
*
* @since 4.0.0
* @throws \Exception
*/
public function move(string $sourcePath, string $destinationPath, bool $force = false): string
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
if (!@ftp_rename($this->ftp_connection, $this->ftp_root . $sourcePath, $this->ftp_root . $destinationPath)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("Unable to rename {$sourcePath} to {$destinationPath}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
return $destinationPath;
}
/**
* Returns a url which can be used to display an image from within the "images" directory.
*
* @param string $path Path of the file relative to adapter
*
* @return string
*
* @since 4.0.0
*/
public function getUrl(string $path): string
{
if (!$this->ftp_connection) {
throw new \Exception("No FTP connection available");
}
if ($this->url_root) {
return $this->url_root . $path;
} else {
$hash = hash("xxh3", $path);
$local_filename = JPATH_ROOT . '/tmp/' . $hash . '.tmp';
if (file_exists($local_filename)) {
return Uri::root() . 'tmp/' . $hash . '.tmp';
} else {
if (!@ftp_get($this->ftp_connection, $local_filename, $this->ftp_root . $path)) {
$message = error_get_last() !== null && error_get_last() !== [] ? error_get_last()['message'] : 'Error';
Log::add("FTP Unable to download {$path} to {$local_filename}: {$message}", Log::ERROR, 'ftp');
throw new \Exception($message);
}
return Uri::root() . 'tmp/' . $hash . '.tmp';
}
}
return '';
}
/**
* Returns the name of this adapter.
*
* @return string
*
* @since 4.0.0
*/
public function getAdapterName(): string
{
return $this->ftp_root;
}
/**
* Search for a pattern in a given path
*
* @param string $path The base path for the search
* @param string $needle The path to file
* @param bool $recursive Do a recursive search
*
* @return \stdClass[]
*
* @since 4.0.0
*/
public function search(string $path, string $needle, bool $recursive = false): array
{
return array();
}
/**
* Creates a safe file name for the given name.
*
* @param string $name The filename
*
* @return string
*
* @since 4.0.0
* @throws \Exception
*/
private function getSafeName(string $name): string
{
// Copied from the Joomla local filesystem plugin code
// Make the filename safe
if (!$name = File::makeSafe($name)) {
throw new \Exception(Text::_('COM_MEDIA_ERROR_MAKESAFE'));
}
// Transform filename to punycode
$name = PunycodeHelper::toPunycode($name);
// Get the extension
$extension = File::getExt($name);
// Normalise extension, always lower case
if ($extension) {
$extension = '.' . strtolower($extension);
}
$nameWithoutExtension = substr($name, 0, \strlen($name) - \strlen($extension));
return $nameWithoutExtension . $extension;
}
}