first commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* FTP/SFTP ADDON
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Duplicator
|
||||
* Author URI: https://duplicator.com/
|
||||
*
|
||||
* PHP version 5.3
|
||||
*
|
||||
* @category Duplicator
|
||||
* @package Plugin
|
||||
* @author Duplicator
|
||||
* @copyright 2011-2021 Snapcreek LLC
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
|
||||
* @version GIT: $Id$
|
||||
* @link https://duplicator.com/
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon;
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\FTPStorage;
|
||||
use Duplicator\Addons\FtpAddon\Models\SFTPStorage;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
/**
|
||||
* Storage ftp/sftp addon class
|
||||
*/
|
||||
class FtpAddon extends AbstractAddonCore
|
||||
{
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
add_action('init', [$this, 'hookInit']);
|
||||
add_action('duplicator_pro_register_storage_types', [$this, 'registerStorages']);
|
||||
add_filter('duplicator_template_file', array(__CLASS__, 'getTemplateFile'), 10, 2);
|
||||
add_filter('duplicator_usage_stats_storages_infos', array(__CLASS__, 'getStorageUsageStats'), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function calle on duplicator_addons_loaded hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function hookInit()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
FTPStorage::registerType();
|
||||
SFTPStorage::registerType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return template file path
|
||||
*
|
||||
* @param string $path path to the template file
|
||||
* @param string $slugTpl slug of the template
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTemplateFile($path, $slugTpl)
|
||||
{
|
||||
if (strpos($slugTpl, 'ftpaddon/') === 0) {
|
||||
return self::getAddonPath() . '/template/' . $slugTpl . '.php';
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage usage stats
|
||||
*
|
||||
* @param array<string,int> $storageNums Storages num
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public static function getStorageUsageStats($storageNums)
|
||||
{
|
||||
if (($storages = AbstractStorageEntity::getAll()) === false) {
|
||||
$storages = [];
|
||||
}
|
||||
|
||||
$storageNums['storages_ftp_count'] = 0;
|
||||
$storageNums['storages_sftp_count'] = 0;
|
||||
|
||||
foreach ($storages as $index => $storage) {
|
||||
switch ($storage->getSType()) {
|
||||
case FTPStorage::getSType():
|
||||
$storageNums['storages_ftp_count']++;
|
||||
break;
|
||||
case SFTPStorage::getSType():
|
||||
$storageNums['storages_sftp_count']++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use CurlHandle;
|
||||
use Duplicator\Addons\FtpAddon\Utils\FTPUtils;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Exception;
|
||||
|
||||
class FTPCurlStorageAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024;
|
||||
/** @var string */
|
||||
private $root = '';
|
||||
/** @var string */
|
||||
private $server = '';
|
||||
/** @var int */
|
||||
private $port = 21;
|
||||
/** @var string */
|
||||
private $username = '';
|
||||
/** @var string */
|
||||
private $password = '';
|
||||
/** @var int */
|
||||
private $timeoutInSec = 15;
|
||||
/** @var bool */
|
||||
private $ssl = false;
|
||||
/** @var bool */
|
||||
private $passiveMode = false;
|
||||
/** @var resource */
|
||||
private $sourceFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastSourceFilePath = '';
|
||||
/** @var resource */
|
||||
private $destFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastDestFilePath = '';
|
||||
/** @var resource */
|
||||
private $tempFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastTempFilePath = '';
|
||||
/** @var null|resource|CurlHandle */
|
||||
private $connection = null;
|
||||
/** @var int */
|
||||
private $throttle = 0;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $server The server to connect to
|
||||
* @param int $port The port to connect to
|
||||
* @param string $username The username to use
|
||||
* @param string $password The password to use
|
||||
* @param string $root The root directory to use
|
||||
* @param int $timeoutInSec The timeout in seconds
|
||||
* @param bool $ssl Whether to use SSL
|
||||
* @param bool $passiveMode Whether to use passive mode
|
||||
* @param int $throttle The throttle in microseconds
|
||||
*/
|
||||
public function __construct(
|
||||
$server,
|
||||
$port = 21,
|
||||
$username = '',
|
||||
$password = '',
|
||||
$root = '/',
|
||||
$timeoutInSec = 15,
|
||||
$ssl = false,
|
||||
$passiveMode = false,
|
||||
$throttle = 0
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->port = (int) $port;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->root = SnapIO::trailingslashit($root);
|
||||
$this->timeoutInSec = max(1, (int) $timeoutInSec);
|
||||
$this->ssl = (bool) $ssl;
|
||||
$this->passiveMode = (bool) $passiveMode;
|
||||
$this->throttle = max(0, (int) $throttle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the FTP connection and initializes root directory
|
||||
*
|
||||
* @param string $errorMsg The error message to return
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function initialize(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->isDir('/') && !$this->createDir('/')) {
|
||||
$errorMsg = "Couldn't create root directory.";
|
||||
return false;
|
||||
}
|
||||
$this->wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle the connection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function wait()
|
||||
{
|
||||
if ($this->throttle > 0) {
|
||||
usleep($this->throttle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid and ready to use.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
$errorMsg = "FTP connection info is invalid: " . $errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->testConnection($errorMsg) === false) {
|
||||
$errorMsg = "FTP connection failed: " . $errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isDir('/')) {
|
||||
$errorMsg = "FTP root directory doesn't exist.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connection info is valid
|
||||
*
|
||||
* @param string $errorMsg The error message to return
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isConnectionInfoValid(&$errorMsg = '')
|
||||
{
|
||||
if (strlen($this->server) < 1) {
|
||||
$errorMsg = "FTP server is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->username) < 1) {
|
||||
$errorMsg = "FTP username is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->password) < 1) {
|
||||
$errorMsg = "FTP password is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->port < 1) {
|
||||
$errorMsg = "FTP port is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->root) < 1) {
|
||||
$errorMsg = "FTP root directory is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* test ftp connection
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function testConnection(&$errorMsg = '')
|
||||
{
|
||||
$path = $this->getFullPath('/', true);
|
||||
return $this->curlCall($path, [CURLOPT_TIMEOUT => $this->timeoutInSec]) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory specified by pathname, recursively if necessary.
|
||||
*
|
||||
* @param string $path The directory path.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realCreateDir($path)
|
||||
{
|
||||
try {
|
||||
$path = SnapIO::trailingslashit($this->getFullPath($path, true));
|
||||
return $this->curlCall($path, [CURLOPT_FTP_CREATE_MISSING_DIRS => true]) !== false;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file with content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
* @param string $content The content of file.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCreateFile($path, $content)
|
||||
{
|
||||
try {
|
||||
if (($fullPath = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->exists($path) && !$this->delete($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'duplicator-pro-');
|
||||
if (($bytesWritten = file_put_contents($tmpFile, $content)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($stream = @fopen($tmpFile, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = $this->curlCall(
|
||||
$fullPath,
|
||||
[
|
||||
CURLOPT_UPLOAD => true,
|
||||
CURLOPT_NOPROGRESS => true,
|
||||
CURLOPT_FTP_CREATE_MISSING_DIRS => true,
|
||||
CURLOPT_INFILE => $stream,
|
||||
CURLOPT_INFILESIZE => $bytesWritten,
|
||||
]
|
||||
);
|
||||
|
||||
@fclose($stream);
|
||||
if ($success === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
*
|
||||
* @return string|false The content of file or false on failure.
|
||||
*/
|
||||
public function getFileContent($path)
|
||||
{
|
||||
if (($path = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($content = $this->curlCall($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move and/or rename a file or directory.
|
||||
*
|
||||
* @param string $oldPath Relative storage path
|
||||
* @param string $newPath Relative storage path
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realMove($oldPath, $newPath)
|
||||
{
|
||||
try {
|
||||
if (($fullOldPath = $this->getFullPath($oldPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullNewPath = $this->getFullPath($newPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["RNFR " . $fullOldPath, "RNTO " . $fullNewPath]]) !== false;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete reletative path from storage root.
|
||||
*
|
||||
* @param string $path The path to delete. (Accepts directories and files)
|
||||
* @param bool $recursive Allows the deletion of nested directories specified in the pathname. Default to false.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realDelete($path, $recursive = false)
|
||||
{
|
||||
try {
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
if ($this->isDir($path)) {
|
||||
$fullPath = SnapIO::trailingslashit($fullPath);
|
||||
if ($recursive) {
|
||||
foreach ($this->scanDir($path) as $item) {
|
||||
if (!$this->realDelete(SnapIO::trailingslashit($path) . $item, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["RMD " . $fullPath]]) !== false;
|
||||
} elseif ($this->isFile($path)) {
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["DELE " . $fullPath]]) !== false;
|
||||
} else {
|
||||
//path doesn't exist
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy local file to storage, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $sourceFile The source file full path
|
||||
* @param string $storageFile Storage destination path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCopyToStorage($sourceFile, $storageFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($storageFileFullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_file($sourceFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && $this->isFile($storageFile) && !$this->delete($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Uplaod file at once without any other operation
|
||||
if (($timeout === 0 && $offset === 0 && $length < 0) || filesize($sourceFile) < $length) {
|
||||
if (($content = @file_get_contents($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->createFile($storageFile, $content);
|
||||
}
|
||||
|
||||
if (
|
||||
($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false ||
|
||||
($tempFileHandle = $this->getTempFileHandle()) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
do {
|
||||
if (
|
||||
@fseek($sourceFileHandle, $offset) === -1 ||
|
||||
($chunk = @fread($sourceFileHandle, $length)) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@ftruncate($tempFileHandle, 0) === false ||
|
||||
@rewind($tempFileHandle) === false ||
|
||||
@fwrite($tempFileHandle, $chunk) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@rewind($tempFileHandle);
|
||||
|
||||
$result = $this->curlCall(
|
||||
$storageFileFullPath,
|
||||
[
|
||||
CURLOPT_FTPAPPEND => true,
|
||||
CURLOPT_UPLOAD => true,
|
||||
CURLOPT_FTP_CREATE_MISSING_DIRS => true,
|
||||
CURLOPT_INFILE => $tempFileHandle,
|
||||
CURLOPT_INFILESIZE => strlen($chunk),
|
||||
]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//abort on first chunk if no timeout
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($chunk);
|
||||
$offset += strlen($chunk);
|
||||
} while ($timeout !== 0 && self::getElapsedTime($startTime) < $timeout && !feof($sourceFileHandle));
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy storage file to local file, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $storageFile The storage file path
|
||||
* @param string $destFile The destination local file full path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
public function copyFromStorage($storageFile, $destFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($fullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wp_mkdir_p(dirname($destFile)) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && @file_exists($destFile) && !@unlink($destFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isFile($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0 && $offset === 0 && $length < 0) {
|
||||
if (($content = $this->getFileContent($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @file_put_contents($destFile, $content);
|
||||
}
|
||||
|
||||
if (($handle = $this->getDestFileHandle($destFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@fseek($handle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
do {
|
||||
if (@fseek($handle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = $this->curlCall(
|
||||
$fullPath,
|
||||
[
|
||||
CURLOPT_RANGE => sprintf('%d-%d', $offset, $offset + $length - 1),
|
||||
]
|
||||
);
|
||||
|
||||
if (
|
||||
$content === false ||
|
||||
(strlen($content) > 0 && @fwrite($handle, $content) === false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += strlen($content);
|
||||
} while ($timeout !== 0 && self::getElapsedTime($startTime) < $timeout && strlen($content) > 0);
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files meta information in a folder
|
||||
*
|
||||
* @param string $path remote dir path
|
||||
* @param bool $filterDots filters . and .. from the list
|
||||
*
|
||||
* @return array{array{name: string, size: int, modified: int, created: int, isDir: bool}}|array{}
|
||||
*/
|
||||
private function getRawListInfo($path, $filterDots = true)
|
||||
{
|
||||
//direactories need the trailing slash to be recognized as such
|
||||
$path = SnapIO::trailingslashit($this->getFullPath($path, true));
|
||||
$res = $this->curlCall($path, [CURLOPT_CUSTOMREQUEST => 'LIST']);
|
||||
|
||||
if ($res === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = explode("\n", $res);
|
||||
$items = array_filter($items, function ($item) {
|
||||
return !empty($item);
|
||||
});
|
||||
|
||||
if (empty($items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (strpos($items[0], 'total') !== false) {
|
||||
array_shift($items);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($items as $key => $item) {
|
||||
if (($parsed = FTPUtils::parseRawListString($item, $this->getSystemType())) !== false) {
|
||||
if ($filterDots && ($parsed['name'] === '.' || $parsed['name'] === '..')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get System type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getSystemType()
|
||||
{
|
||||
if (($res = $this->curlCall('/', [CURLOPT_CUSTOMREQUEST => 'SYST'])) === false) {
|
||||
return 'UNIX';
|
||||
}
|
||||
|
||||
$res = strtoupper($res);
|
||||
if (strpos($res, 'WINDOWS') !== false) {
|
||||
return 'WINDOWS_NT';
|
||||
}
|
||||
|
||||
return 'UNIX';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo|false The path info or false if path is invalid.
|
||||
*/
|
||||
public function getRealPathInfo($path)
|
||||
{
|
||||
if (($fullPath = $this->getFullPath($path, true)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = $path;
|
||||
|
||||
if (
|
||||
($response = $this->curlCall($fullPath, [CURLOPT_HEADER => true, CURLOPT_NOBODY => true])) !== false &&
|
||||
preg_match('/^Content-Length:\s*(\d+)/im', $response, $matches) === 1
|
||||
) {
|
||||
// Is file
|
||||
$info->exists = true;
|
||||
$info->isDir = false;
|
||||
$info->size = (int) $matches[1];
|
||||
|
||||
$response = $this->curlCall($fullPath, [CURLOPT_CUSTOMREQUEST => 'MDTM']);
|
||||
$matches = [];
|
||||
if (preg_match('/^(\d{14})/', $response, $matches) === 1) {
|
||||
$info->modified = strtotime($matches[1]);
|
||||
} else {
|
||||
$info->modified = time();
|
||||
}
|
||||
|
||||
$info->created = $info->modified;
|
||||
} elseif ($this->curlCall(trailingslashit($fullPath), [CURLOPT_CUSTOMREQUEST => 'NLST']) !== false) {
|
||||
// Is folder
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = $path;
|
||||
$info->exists = true;
|
||||
$info->isDir = true;
|
||||
$info->size = 0;
|
||||
$info->created = time();
|
||||
$info->modified = time();
|
||||
} else {
|
||||
// Not exists
|
||||
$info->exists = false;
|
||||
$info->isDir = false;
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of files and directories inside the specified path.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, scan root path.
|
||||
* @param bool $files If true, add files to the list. Default to true.
|
||||
* @param bool $folders If true, add folders to the list. Default to true.
|
||||
*
|
||||
* @return string[] The list of files and directories, empty array if path is invalid.
|
||||
*/
|
||||
public function scanDir($path, $files = true, $folders = true)
|
||||
{
|
||||
$infoList = $this->getRawListInfo($path);
|
||||
$result = [];
|
||||
foreach ($infoList as $item) {
|
||||
if ($item['isDir'] && !$folders) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$item['isDir'] && !$files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = $item['name'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if directory is empty.
|
||||
*
|
||||
* @param string $path The folder path
|
||||
* @param string[] $filters Filters to exclude files and folders from the check, if start and end with /, use regex.
|
||||
*
|
||||
* @return bool True is ok, false otherwise
|
||||
*/
|
||||
public function isDirEmpty($path, $filters = [])
|
||||
{
|
||||
if (!$this->isDir($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$regexFilters = [];
|
||||
$normalFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^\/.*\/$/', $filter) === 1) {
|
||||
$regexFilters[] = $filter;
|
||||
} else {
|
||||
$normalFilters[] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
$contents = $this->scanDir($path);
|
||||
foreach ($contents as $item) {
|
||||
if (in_array($item, $normalFilters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($regexFilters as $regexFilter) {
|
||||
if (preg_match($regexFilter, $item) === 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
// Don't delete if root directory
|
||||
if (
|
||||
preg_match('/^[a-zA-Z]:\/$/', $this->root) === 1 ||
|
||||
preg_match('/^\/$/', $this->root) === 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->delete('/', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destruct
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (is_resource($this->tempFileHandle)) {
|
||||
fclose($this->tempFileHandle);
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
|
||||
if ($this->connection !== null) {
|
||||
curl_close($this->connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a curl call
|
||||
*
|
||||
* @param string $path where the curl call occur
|
||||
* @param array<int,mixed> $options configuration options
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return string|false response or false on failure
|
||||
*/
|
||||
private function curlCall($path = '/', $options = [], &$errorMsg = '')
|
||||
{
|
||||
if ($this->connection === null) {
|
||||
$this->connection = curl_init();
|
||||
} else {
|
||||
curl_reset($this->connection);
|
||||
}
|
||||
|
||||
if ($this->connection === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = ltrim($path, '/\\');
|
||||
$options[CURLOPT_URL] = sprintf('ftp://%s:%d/%s', $this->server, $this->port, $path);
|
||||
$options = array_replace($this->getDefaultOptions(), $options);
|
||||
|
||||
curl_setopt_array($this->connection, $options);
|
||||
|
||||
if (($response = curl_exec($this->connection)) === false) {
|
||||
if (($errno = curl_errno($this->connection))) {
|
||||
switch ($errno) {
|
||||
case 6:
|
||||
case 7:
|
||||
$errorMsg = 'Unable to connect to FTP server. Please check your FTP hostname, port, and active mode settings. Error code: ' . $errno;
|
||||
break;
|
||||
case 8:
|
||||
$errorMsg = 'Got an unexpected reply from FTP server. Error code: ' . $errno;
|
||||
break;
|
||||
case 9:
|
||||
$errorMsg = 'Unable to change FTP directory. Please ensure that you have permission on the server. Error code: ' . $errno;
|
||||
break;
|
||||
case 23:
|
||||
$errorMsg = 'Unable to download file from FTP server. Please ensure that you have enough disk space. Error code: ' . $errno;
|
||||
break;
|
||||
case 28:
|
||||
$errorMsg = 'Connecting to FTP server timed out. Please check FTP hostname, port, username, password, and active mode ' .
|
||||
'settings. Error code: ' . $errno;
|
||||
break;
|
||||
case 67:
|
||||
$errorMsg = 'Unable to login to FTP server. Please check your username and password. Error code: ' . $errno;
|
||||
break;
|
||||
default:
|
||||
$errorMsg = 'Unable to connect to FTP. Error code: ' . $errno . '. Error message: ' . curl_error($this->connection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$http_code = curl_getinfo($this->connection, CURLINFO_HTTP_CODE);
|
||||
if ($http_code >= 400) {
|
||||
$errorMsg = sprintf('Error code: %s.', $http_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns default options for cURL
|
||||
*
|
||||
* @return array<int,mixed>
|
||||
*/
|
||||
private function getDefaultOptions()
|
||||
{
|
||||
$options = [
|
||||
CURLOPT_USERPWD => sprintf('%s:%s', $this->username, $this->password),
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_BINARYTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $this->timeoutInSec,
|
||||
CURLOPT_TIMEOUT => $this->timeoutInSec,
|
||||
];
|
||||
|
||||
|
||||
if ($this->ssl) {
|
||||
$options[CURLOPT_SSL_VERIFYPEER] = false;
|
||||
$options[CURLOPT_SSL_VERIFYHOST] = false;
|
||||
$options[CURLOPT_FTP_SSL] = CURLFTPSSL_TRY;
|
||||
$options[CURLOPT_FTPSSLAUTH] = CURLFTPAUTH_TLS;
|
||||
}
|
||||
|
||||
if ($this->passiveMode) {
|
||||
$options[CURLOPT_FTP_USE_EPSV] = true;
|
||||
} else {
|
||||
$options[CURLOPT_FTP_USE_EPRT] = true;
|
||||
$options[CURLOPT_FTPPORT] = 0;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $destFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getDestFileHandle($destFilePath)
|
||||
{
|
||||
if ($this->lastDestFilePath === $destFilePath) {
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
|
||||
if (($this->destFileHandle = SnapIO::fopen($destFilePath, 'cb')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastDestFilePath = $destFilePath;
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $sourceFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getSourceFileHandle($sourceFilePath)
|
||||
{
|
||||
if ($this->lastSourceFilePath === $sourceFilePath) {
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (($this->sourceFileHandle = SnapIO::fopen($sourceFilePath, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastSourceFilePath = $sourceFilePath;
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty file stream to temporarlly store chunk data.
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
private function getTempFileHandle()
|
||||
{
|
||||
if (is_resource($this->tempFileHandle)) {
|
||||
if (ftruncate($this->tempFileHandle, 0) === false) {
|
||||
return false;
|
||||
}
|
||||
if (rewind($this->tempFileHandle) === false) {
|
||||
return false;
|
||||
}
|
||||
return $this->tempFileHandle;
|
||||
}
|
||||
|
||||
if (@file_exists($this->lastTempFilePath)) {
|
||||
@unlink($this->lastTempFilePath);
|
||||
}
|
||||
|
||||
$this->lastTempFilePath = tempnam(sys_get_temp_dir(), 'duplicator_ftp_curl_tmpfile_');
|
||||
if (($this->tempFileHandle = @fopen($this->lastTempFilePath, 'r+')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->tempFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full path of storage from relative path.
|
||||
*
|
||||
* @param string $path The relative storage path
|
||||
* @param bool $acceptEmpty If true, return root path if path is empty. Default to false.
|
||||
*
|
||||
* @return string|false The full path or false if path is invalid.
|
||||
*/
|
||||
protected function getFullPath($path, $acceptEmpty = false)
|
||||
{
|
||||
$path = ltrim((string) $path, '/\\');
|
||||
if (strlen($path) === 0) {
|
||||
return $acceptEmpty ? SnapIO::trailingslashit($this->root) : false;
|
||||
}
|
||||
return $this->root . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elapsed time in microseconds
|
||||
*
|
||||
* @param float $startTime start time in microseconds
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function getElapsedTime($startTime)
|
||||
{
|
||||
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\DynamicGlobalEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Exception;
|
||||
|
||||
class FTPStorage extends AbstractStorageEntity
|
||||
{
|
||||
const MIN_UPLOAD_CHUNK_SIZE_IN_MB = 1;
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB = 5;
|
||||
const MAX_UPLOAD_CHUNK_SIZE_IN_MB = 100;
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'server' => '',
|
||||
'port' => 21,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'use_curl' => false,
|
||||
'timeout_in_secs' => 15,
|
||||
'ssl' => false,
|
||||
'passive_mode' => true,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get stoage adapter
|
||||
*
|
||||
* @return AbstractStorageAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
if ($this->adapter !== null) {
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
if ($this->config['use_curl']) {
|
||||
$this->adapter = new FTPCurlStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['timeout_in_secs'],
|
||||
$this->config['ssl'],
|
||||
$this->config['passive_mode']
|
||||
);
|
||||
} else {
|
||||
$this->adapter = new FTPStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['timeout_in_secs'],
|
||||
$this->config['ssl'],
|
||||
$this->config['passive_mode']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called, automatically, when Serialize
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize() // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound
|
||||
{
|
||||
$data = parent::__serialize();
|
||||
unset($data['client']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
parent::__wakeup();
|
||||
|
||||
if ($this->legacyEntity) {
|
||||
// Old storage entity
|
||||
$this->legacyEntity = false;
|
||||
// Make sure the storage type is right from the old entity
|
||||
$this->storage_type = $this->getSType();
|
||||
$this->config = [
|
||||
'server' => $this->ftp_server,
|
||||
'port' => $this->ftp_port,
|
||||
'username' => $this->ftp_username,
|
||||
'password' => $this->ftp_password,
|
||||
'use_curl' => $this->ftp_use_curl,
|
||||
'storage_folder' => '/' . ltrim($this->ftp_storage_folder, '/\\'),
|
||||
'max_packages' => $this->ftp_max_files,
|
||||
'timeout_in_secs' => $this->ftp_timeout_in_secs,
|
||||
'ssl' => $this->ftp_ssl,
|
||||
'passive_mode' => $this->ftp_passive_mode,
|
||||
];
|
||||
// reset old values
|
||||
$this->ftp_server = '';
|
||||
$this->ftp_port = 21;
|
||||
$this->ftp_username = '';
|
||||
$this->ftp_password = '';
|
||||
$this->ftp_use_curl = false;
|
||||
$this->ftp_storage_folder = '';
|
||||
$this->ftp_max_files = 10;
|
||||
$this->ftp_timeout_in_secs = 15;
|
||||
$this->ftp_ssl = false;
|
||||
$this->ftp_passive_mode = false;
|
||||
}
|
||||
|
||||
// For legacy entities, we need to make sure the config is up to date
|
||||
$this->config['port'] = (int) $this->config['port'];
|
||||
$this->config['max_packages'] = (int) $this->config['max_packages'];
|
||||
$this->config['timeout_in_secs'] = (int) $this->config['timeout_in_secs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FontAwesome storage type icon.
|
||||
*
|
||||
* @return string Returns the font-awesome icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
return '<i class="fas fa-network-wired fa-fw"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('FTP', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return "ftp://" . $this->config['server'] . ":" . $this->config['port'] . $this->getStorageFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return apply_filters('duplicator_pro_ftp_connect_exists', function_exists('ftp_connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
esc_html__(
|
||||
'FTP Storage requires FTP module enabled. Please install the FTP module as described in the %s.',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="https://secure.php.net/manual/en/ftp.installation.php" target="_blank">https://secure.php.net/manual/en/ftp.installation.php</a>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->getAdapter()->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
return $dGlobal->getVal('ftp_upload_chunksize_in_mb', self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB) * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
return (int) ($this->config['timeout_in_secs'] <= 0 ? 0 : $this->config['timeout_in_secs'] * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action key text
|
||||
*
|
||||
* @param string $key Key name (action, pending, failed, cancelled, success)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getActionKeyText($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'action':
|
||||
return sprintf(
|
||||
__('Transferring to FTP server %1$s in folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to FTP server %1$s in folder %2$s is pending', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to FTP server %1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to FTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to FTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
default:
|
||||
throw new Exception('Invalid key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List quick view
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getListQuickView($echo = true)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<div>
|
||||
<label><?php esc_html_e('Server', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($this->config['server']); ?>: <?php echo intval($this->config['port']); ?> <br/>
|
||||
<label><?php esc_html_e('Location', 'duplicator-pro') ?>:</label>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$this->getHtmlLocationLink(),
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
if ($echo) {
|
||||
ob_end_flush();
|
||||
return '';
|
||||
} else {
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
return TplMng::getInstance()->render(
|
||||
'ftpaddon/configs/ftp',
|
||||
[
|
||||
'storage' => $this,
|
||||
'server' => $this->config['server'],
|
||||
'port' => $this->config['port'],
|
||||
'username' => $this->config['username'],
|
||||
'password' => $this->config['password'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'timeout' => $this->config['timeout_in_secs'],
|
||||
'useCurl' => $this->config['use_curl'],
|
||||
'isPassive' => $this->config['passive_mode'],
|
||||
'useSSL' => $this->config['ssl'],
|
||||
],
|
||||
$echo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data from http request, this method don't save data, just update object properties
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if success and all data is valid, false otherwise
|
||||
*/
|
||||
public function updateFromHttpRequest(&$message = '')
|
||||
{
|
||||
if ((parent::updateFromHttpRequest($message) === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'ftp_max_files', 10);
|
||||
$this->config['server'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_server', '');
|
||||
$this->config['port'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'ftp_port', 10);
|
||||
$this->config['username'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_username', '');
|
||||
$password = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_password', '');
|
||||
$password2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_password2', '');
|
||||
if (strlen($password) > 0) {
|
||||
if ($password !== $password2) {
|
||||
$message = __('Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['password'] = $password;
|
||||
}
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_ftp_storage_folder', 'add');
|
||||
$this->config['timeout_in_secs'] = max(10, SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'ftp_timeout_in_secs', 15));
|
||||
$this->config['use_curl'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_use_curl', false);
|
||||
$this->config['ssl'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_ssl', false);
|
||||
$this->config['passive_mode'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_passive_mode', false);
|
||||
|
||||
$errorMsg = '';
|
||||
if ($this->getAdapter()->initialize($errorMsg) === false) {
|
||||
$message = sprintf(
|
||||
__('Failed to connect to FTP server with message: %1$s', 'duplicator-pro'),
|
||||
$errorMsg
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('FTP Storage Updated - Server %1$s, Folder %2$s was created.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerType()
|
||||
{
|
||||
parent::registerType();
|
||||
|
||||
add_action('duplicator_update_global_storage_settings', function () {
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
|
||||
foreach (static::getDefaultSettings() as $key => $default) {
|
||||
$value = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, $key, $default);
|
||||
$dGlobal->setVal($key, $value);
|
||||
}
|
||||
$dGlobal->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
protected static function getDefaultSettings()
|
||||
{
|
||||
return ['ftp_upload_chunksize_in_mb' => self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page for this storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function renderGlobalOptions()
|
||||
{
|
||||
$values = self::getDefaultSettings();
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
foreach ($values as $key => $default) {
|
||||
$values[$key] = $dGlobal->getVal($key, $default);
|
||||
}
|
||||
?>
|
||||
<h3 class="title"><?php esc_html_e("FTP", 'duplicator-pro') ?></h3>
|
||||
<hr size="1" />
|
||||
<table class="form-table">
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Upload Size (MB)", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input class="dup-narrow-input"
|
||||
type="number"
|
||||
min="<?php echo self::MIN_UPLOAD_CHUNK_SIZE_IN_MB; ?>"
|
||||
max="<?php echo self::MAX_UPLOAD_CHUNK_SIZE_IN_MB; ?>"
|
||||
name="ftp_upload_chunksize_in_mb"
|
||||
id="ftp_upload_chunksize_in_mb"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#ftp_upload_chunksize_in_mb_error_container"
|
||||
value="<?php echo (int) $values['ftp_upload_chunksize_in_mb']; ?>" />
|
||||
<div id="ftp_upload_chunksize_in_mb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be uploaded to the server per attempt.', 'duplicator-pro'); ?>
|
||||
<?php echo esc_html(sprintf(__('Min size %smb.', 'duplicator-pro'), self::MIN_UPLOAD_CHUNK_SIZE_IN_MB)); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Exception;
|
||||
|
||||
class SFTPStorage extends AbstractStorageEntity
|
||||
{
|
||||
const MIN_UPLOAD_CHUNK_SIZE_IN_MB = 1;
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB = 5;
|
||||
const MAX_UPLOAD_CHUNK_SIZE_IN_MB = 100;
|
||||
|
||||
/**
|
||||
* Get stoage adapter
|
||||
*
|
||||
* @return SFTPStorageAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
return new SFTPStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['private_key'],
|
||||
$this->config['private_key_password'],
|
||||
$this->config['timeout_in_secs']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'server' => '',
|
||||
'port' => 22,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'private_key' => '',
|
||||
'private_key_password' => '',
|
||||
'timeout_in_secs' => 15,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
parent::__wakeup();
|
||||
|
||||
if ($this->legacyEntity) {
|
||||
// Old storage entity
|
||||
$this->legacyEntity = false;
|
||||
// Make sure the storage type is right from the old entity
|
||||
$this->storage_type = $this->getSType();
|
||||
$this->config = [
|
||||
'server' => $this->sftp_server,
|
||||
'port' => $this->sftp_port,
|
||||
'username' => $this->sftp_username,
|
||||
'password' => $this->sftp_password,
|
||||
'private_key' => $this->sftp_private_key,
|
||||
'private_key_password' => $this->sftp_private_key_password,
|
||||
'storage_folder' => '/' . ltrim($this->sftp_storage_folder, '/\\'),
|
||||
'max_packages' => $this->sftp_max_files,
|
||||
'timeout_in_secs' => $this->sftp_timeout_in_secs,
|
||||
];
|
||||
// reset old values
|
||||
$this->sftp_server = '';
|
||||
$this->sftp_port = 22;
|
||||
$this->sftp_username = '';
|
||||
$this->sftp_password = '';
|
||||
$this->sftp_private_key = '';
|
||||
$this->sftp_private_key_password = '';
|
||||
$this->sftp_storage_folder = '';
|
||||
$this->sftp_max_files = 10;
|
||||
$this->sftp_timeout_in_secs = 15;
|
||||
$this->sftp_disable_chunking_mode = false;
|
||||
}
|
||||
|
||||
// For legacy entities, we need to make sure the config is up to date
|
||||
$this->config['port'] = (int) $this->config['port'];
|
||||
$this->config['max_packages'] = (int) $this->config['max_packages'];
|
||||
$this->config['timeout_in_secs'] = (int) $this->config['timeout_in_secs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FontAwesome storage type icon.
|
||||
*
|
||||
* @return string Returns the font-awesome icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
return '<i class="fas fa-network-wired fa-fw"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('SFTP', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return $this->config['server'] . ":" . $this->config['port'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('gmp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
_x(
|
||||
'SFTP requires the %1$sgmp extension%2$s. Please contact your host to install.',
|
||||
'1: <a> tag, 2: </a> tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="http://php.net/manual/en/book.gmp.php" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->getAdapter()->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action key text
|
||||
*
|
||||
* @param string $key Key name (action, pending, failed, cancelled, success)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getActionKeyText($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'action':
|
||||
return sprintf(
|
||||
__('Transferring to SFTP server %1$s in folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to SFTP server %1$s in folder %2$s is pending', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to SFTP server %1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to SFTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to SFTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
default:
|
||||
throw new Exception('Invalid key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List quick view
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getListQuickView($echo = true)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<div>
|
||||
<label><?php esc_html_e('Server', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($this->config['server']); ?>: <?php echo intval($this->config['port']); ?> <br/>
|
||||
<label><?php esc_html_e('Location', 'duplicator-pro') ?>:</label>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$this->getHtmlLocationLink(),
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
if ($echo) {
|
||||
ob_end_flush();
|
||||
return '';
|
||||
} else {
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
return TplMng::getInstance()->render(
|
||||
'ftpaddon/configs/sftp',
|
||||
[
|
||||
'storage' => $this,
|
||||
'server' => $this->config['server'],
|
||||
'port' => $this->config['port'],
|
||||
'username' => $this->config['username'],
|
||||
'password' => $this->config['password'],
|
||||
'privateKey' => $this->config['private_key'],
|
||||
'privateKeyPwd' => $this->config['private_key_password'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'timeout' => $this->config['timeout_in_secs'],
|
||||
],
|
||||
$echo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
return (int) ($this->config['timeout_in_secs'] <= 0 ? 0 : $this->config['timeout_in_secs'] * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data from http request, this method don't save data, just update object properties
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if success and all data is valid, false otherwise
|
||||
*/
|
||||
public function updateFromHttpRequest(&$message = '')
|
||||
{
|
||||
if ((parent::updateFromHttpRequest($message) === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'sftp_max_files', 10);
|
||||
$this->config['server'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_server', '');
|
||||
$this->config['port'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'sftp_port', 10);
|
||||
$this->config['username'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_username', '');
|
||||
$password = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_password', '');
|
||||
$password2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_password2', '');
|
||||
$this->config['private_key'] = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key', '');
|
||||
$keyPassword = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key_password', '');
|
||||
$keyPassword2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key_password2', '');
|
||||
if (strlen($password) > 0) {
|
||||
if ($password !== $password2) {
|
||||
$message = __('Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['password'] = $password;
|
||||
} elseif (strlen($keyPassword) > 0) {
|
||||
if ($keyPassword !== $keyPassword2) {
|
||||
$message = __('Priva key Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['private_key_password'] = $keyPassword;
|
||||
}
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_sftp_storage_folder', 'add');
|
||||
$this->config['timeout_in_secs'] = max(10, SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'sftp_timeout_in_secs', 15));
|
||||
|
||||
$errorMsg = '';
|
||||
if ($this->getAdapter()->initialize($errorMsg) === false) {
|
||||
$message = sprintf(
|
||||
__('Failed to connect to SFTP server with message: %1$s', 'duplicator-pro'),
|
||||
$errorMsg
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('SFTP Storage Updated - Server %1$s, Folder %2$s was created.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snapcreek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Exception;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use VendorDuplicator\phpseclib3\Crypt\Common\PrivateKey;
|
||||
use VendorDuplicator\phpseclib3\Crypt\PublicKeyLoader;
|
||||
use VendorDuplicator\phpseclib3\Net\SFTP;
|
||||
|
||||
/**
|
||||
* SFTP class adapter
|
||||
*/
|
||||
class SFTPStorageAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024;
|
||||
/** @var string */
|
||||
protected $server = '';
|
||||
/** @var int */
|
||||
protected $port = 22;
|
||||
/** @var string */
|
||||
protected $username = '';
|
||||
/** @var string */
|
||||
protected $password = '';
|
||||
/** @var string */
|
||||
protected $root = '';
|
||||
/** @var string */
|
||||
protected $privateKey = '';
|
||||
/** @var string */
|
||||
protected $privateKeyPassword = '';
|
||||
/** @var int */
|
||||
protected $timeout = 15;
|
||||
/** @var resource */
|
||||
private $sourceFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastSourceFilePath = '';
|
||||
/** @var resource */
|
||||
private $destFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastDestFilePath = '';
|
||||
/** @var ?SFTP */
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Class contructor
|
||||
*
|
||||
* @param string $server hosting domain or ip address
|
||||
* @param int $port hosting port
|
||||
* @param string $username hosting username
|
||||
* @param string $password hosting password
|
||||
* @param string $root hosting root path
|
||||
* @param string $privateKey hosting private key
|
||||
* @param string $privateKeyPassword hosting private key password
|
||||
* @param int $timeout hosting timeout
|
||||
*/
|
||||
public function __construct(
|
||||
$server,
|
||||
$port = 22,
|
||||
$username = '',
|
||||
$password = '',
|
||||
$root = '',
|
||||
$privateKey = '',
|
||||
$privateKeyPassword = '',
|
||||
$timeout = 15
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->port = (int) $port;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->root = SnapIO::trailingslashit($root);
|
||||
$this->privateKey = $privateKey;
|
||||
$this->privateKeyPassword = $privateKeyPassword;
|
||||
$this->timeout = (int) $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the storage on creation.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function initialize(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->isDir('/') && !$this->createDir('/')) {
|
||||
$errorMsg = 'Could not create root directory';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
return $this->delete('/', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid and ready to use.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getConnection($errorMsg) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isDir('/')) {
|
||||
$errorMsg = 'Root path is invalid';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connection info is valid
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return bool true if connection info is valid, false otherwise
|
||||
*/
|
||||
private function isConnectionInfoValid(&$errorMsg = '')
|
||||
{
|
||||
try {
|
||||
if (strlen($this->server) == 0) {
|
||||
throw new Exception('Server name is required to make sftp connection');
|
||||
}
|
||||
|
||||
if ($this->port < 1) {
|
||||
throw new Exception('Server port is required to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->username) == 0) {
|
||||
throw new Exception('Username is required to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->password) == 0 && strlen($this->privateKey) == 0) {
|
||||
throw new Exception('You should provide either sftp user pasword or the private key to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->privateKey) > 0 && strlen($this->privateKeyPassword) == 0) {
|
||||
throw new Exception('You should provide private key password');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory specified by pathname, recursively if necessary.
|
||||
*
|
||||
* @param string $path The directory path.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function realCreateDir($path)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isDir($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$path = $this->getFullPath($path, true);
|
||||
try {
|
||||
return $conn->mkdir($path, -1, true) !== false;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file with content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
* @param string $content The content of file.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
public function realCreateFile($path, $content)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullPath = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$parentDir = dirname($path);
|
||||
if ($this->createDir($parentDir) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($conn->put($fullPath, $content) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strlen($content);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
*
|
||||
* @return string|false The content of file or false on failure.
|
||||
*/
|
||||
public function getFileContent($path)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($path = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $conn->get($path);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move and/or rename a file or directory.
|
||||
*
|
||||
* @param string $oldPath Relative storage path
|
||||
* @param string $newPath Relative storage path
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function realMove($oldPath, $newPath)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($oldPath = $this->getFullPath($oldPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($newPath = $this->getFullPath($newPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $conn->rename($oldPath, $newPath);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo|false The path info or false if path is invalid.
|
||||
*/
|
||||
public function getRealPathInfo($path)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
try {
|
||||
$info = $conn->stat($fullPath);
|
||||
if ($info === false) {
|
||||
throw new Exception('Could not get path info');
|
||||
}
|
||||
|
||||
$pathInfo = new StoragePathInfo();
|
||||
$pathInfo->exists = true;
|
||||
$pathInfo->path = $path;
|
||||
$pathInfo->isDir = $info['type'] === 2;
|
||||
$pathInfo->size = $pathInfo->isDir ? 0 : $info['size'];
|
||||
$pathInfo->modified = $info['mtime'];
|
||||
$pathInfo->created = isset($info['ctime']) ? $info['ctime'] : $info['mtime'];
|
||||
|
||||
return $pathInfo;
|
||||
} catch (Exception $e) {
|
||||
$pathInfo = new StoragePathInfo();
|
||||
$pathInfo->path = $path;
|
||||
|
||||
return $pathInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of files and directories inside the specified path.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, scan root path.
|
||||
* @param bool $files If true, add files to the list. Default to true.
|
||||
* @param bool $folders If true, add folders to the list. Default to true.
|
||||
*
|
||||
* @return string[] The list of files and directories, empty array if path is invalid.
|
||||
*/
|
||||
public function scanDir($path, $files = true, $folders = true)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = $this->getFullPath($path, true);
|
||||
try {
|
||||
$list = $conn->nlist($path);
|
||||
if ($list === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemPath = SnapIO::trailingslashit($path) . $item;
|
||||
if ($conn->is_dir($itemPath)) {
|
||||
if ($folders) {
|
||||
$result[] = $item;
|
||||
}
|
||||
} else {
|
||||
if ($files) {
|
||||
$result[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if directory is empty.
|
||||
*
|
||||
* @param string $path The folder path
|
||||
* @param string[] $filters Filters to exclude files and folders from the check, if start and end with /, use regex.
|
||||
*
|
||||
* @return bool True is ok, false otherwise
|
||||
*/
|
||||
public function isDirEmpty($path, $filters = [])
|
||||
{
|
||||
if ($this->isDir($path) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$regexFilters = [];
|
||||
$normalFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^\/.*\/$/', $filter) === 1) {
|
||||
$regexFilters[] = $filter;
|
||||
} else {
|
||||
$normalFilters[] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
$contents = $this->scanDir($path);
|
||||
foreach ($contents as $item) {
|
||||
if (in_array($item, $normalFilters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($regexFilters as $regexFilter) {
|
||||
if (preg_match($regexFilter, $item) === 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy local file to storage, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $sourceFile The source file full path
|
||||
* @param string $storageFile Storage destination path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCopyToStorage($sourceFile, $storageFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($storageFileFullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parentDir = dirname($storageFile);
|
||||
if ($offset === 0 && !$this->createDir($parentDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//Uplaod file at once without any other operation
|
||||
if (($timeout === 0 && $offset === 0 && $length < 0) || filesize($sourceFile) < $length) {
|
||||
if (($content = file_get_contents($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->createFile($storageFile, $content);
|
||||
}
|
||||
|
||||
if (($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@fseek($sourceFileHandle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
try {
|
||||
$result = $conn->put(
|
||||
$storageFileFullPath,
|
||||
function ($size) use ($timeout, $startTime, &$bytesWritten, $sourceFileHandle, $length) {
|
||||
if ($timeout !== 0 && (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS > $timeout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($timeout === 0 && $bytesWritten >= $length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (feof($sourceFileHandle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($data = @fread($sourceFileHandle, $size)) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
},
|
||||
SFTP::SOURCE_CALLBACK | SFTP::RESUME,
|
||||
$offset,
|
||||
-1,
|
||||
function ($sent) use (&$bytesWritten) {
|
||||
$bytesWritten = $sent;
|
||||
}
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $timeout === 0 ? $length : $bytesWritten;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Copy storage file to local file, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $storageFile The storage file path
|
||||
* @param string $destFile The destination local file full path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
public function copyFromStorage($storageFile, $destFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wp_mkdir_p(dirname($destFile)) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && @file_exists($destFile) && !@unlink($destFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isFile($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && $length < 0) {
|
||||
if (($content = $this->getFileContent($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @file_put_contents($destFile, $content);
|
||||
}
|
||||
|
||||
if (($handle = $this->getDestFileHandle($destFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
try {
|
||||
do {
|
||||
$content = $conn->get($fullPath, false, $offset, $length);
|
||||
|
||||
if (
|
||||
$content === false ||
|
||||
@fseek($handle, $offset) === -1 ||
|
||||
@fwrite($handle, $content) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += strlen($content);
|
||||
} while (self::getElapsedTime($startTime) < $timeout && $content !== false);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an SFTP object
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return ?SFTP
|
||||
*/
|
||||
private function getConnection(&$errorMsg = '')
|
||||
{
|
||||
|
||||
if ($this->connection instanceof SFTP) {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
throw new Exception($errorMsg);
|
||||
}
|
||||
|
||||
$this->connection = new SFTP($this->server, $this->port);
|
||||
|
||||
if (strlen($this->privateKey) > 0) {
|
||||
if (($key = $this->getPrivateKey()) === null) {
|
||||
throw new Exception('Invalid private key');
|
||||
}
|
||||
|
||||
if (!$this->connection->login($this->username, $key)) {
|
||||
throw new Exception('Invalid username or private key');
|
||||
}
|
||||
} else {
|
||||
if (!$this->connection->login($this->username, $this->password)) {
|
||||
throw new Exception('Invalid username or password');
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if ($this->connection !== null && $this->connection->isConnected()) {
|
||||
$this->connection->disconnect();
|
||||
}
|
||||
$this->connection = null;
|
||||
$errorMsg = $e->getMessage();
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an SFTP Private Key
|
||||
*
|
||||
* @return ?PrivateKey return key object or null
|
||||
*/
|
||||
protected function getPrivateKey()
|
||||
{
|
||||
if (strlen($this->privateKey) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$password = strlen($this->privateKeyPassword) > 0 ? $this->privateKeyPassword : false;
|
||||
$key = PublicKeyLoader::load($this->privateKey, $password);
|
||||
|
||||
if ($key instanceof PrivateKey) {
|
||||
return $key;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete reletative path from storage root.
|
||||
*
|
||||
* @param string $path The path to delete. (Accepts directories and files)
|
||||
* @param bool $recursive Allows the deletion of nested directories specified in the pathname. Default to false.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function realDelete($path, $recursive = false)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->exists($path) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
try {
|
||||
// have to use hack below because phpseclib doesn't work well with
|
||||
// directories in none recursive mode
|
||||
$isDir = $this->isDir($path);
|
||||
$isEmptyDir = $isDir && $this->isDirEmpty($path);
|
||||
if ($isDir) {
|
||||
if ($isEmptyDir === false && $recursive === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $conn->delete($fullPath, true);
|
||||
} else {
|
||||
return $conn->delete($fullPath);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full path of storage from relative path.
|
||||
*
|
||||
* @param string $path The relative storage path
|
||||
* @param bool $acceptEmpty If true, return root path if path is empty. Default to false.
|
||||
*
|
||||
* @return string|false The full path or false if path is invalid.
|
||||
*/
|
||||
protected function getFullPath($path, $acceptEmpty = false)
|
||||
{
|
||||
$path = ltrim((string) $path, '/\\');
|
||||
if (strlen($path) === 0) {
|
||||
return $acceptEmpty ? SnapIO::untrailingslashit($this->root) : false;
|
||||
}
|
||||
return $this->root . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $destFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getDestFileHandle($destFilePath)
|
||||
{
|
||||
if ($this->lastDestFilePath === $destFilePath) {
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
|
||||
if (($this->destFileHandle = @fopen($destFilePath, 'cb')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastDestFilePath = $destFilePath;
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $sourceFilePath The source file path
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
private function getSourceFileHandle($sourceFilePath)
|
||||
{
|
||||
if ($this->lastSourceFilePath === $sourceFilePath) {
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
@fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (($this->sourceFileHandle = @fopen($sourceFilePath, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastSourceFilePath = $sourceFilePath;
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get elapsed time in microseconds
|
||||
*
|
||||
* @param float $startTime start time
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function getElapsedTime($startTime)
|
||||
{
|
||||
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->connection !== null && $this->connection->isConnected()) {
|
||||
$this->connection->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snapcreek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Utils;
|
||||
|
||||
class FTPUtils
|
||||
{
|
||||
const SYS_TYPE_UNIX = 'UNIX';
|
||||
const SYS_TYPE_WINDOWS_NT = 'WINDOWS_NT';
|
||||
|
||||
/**
|
||||
* Parses a raw list item string into an array
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
* @param string $systemType System type (UNIX or WINDOWS_NT)
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
public static function parseRawListString($rawListString, $systemType)
|
||||
{
|
||||
switch (strtoupper($systemType)) {
|
||||
case self::SYS_TYPE_UNIX:
|
||||
return self::parseUnixRawListString($rawListString);
|
||||
case self::SYS_TYPE_WINDOWS_NT:
|
||||
return self::parseWindowsRawListString($rawListString);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info in the raw list item for Windows
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
private static function parseWindowsRawListString($rawListString)
|
||||
{
|
||||
//Regex below gets info from raw string on Windows systems
|
||||
//Dir: 01-01-70 12:00AM <DIR> folder
|
||||
//File: 01-01-70 12:00AM 4096 file.txt
|
||||
//Groups: 1 3 4
|
||||
$regex = '/^(\d{2}-\d{2}-\d{2,4}\s+\d{1,2}:\d{1,2}(?:AM|PM))\s+(?:<DIR>|(\d+))\s+(.+)$/';
|
||||
if (preg_match($regex, $rawListString, $matches) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = [];
|
||||
$info['name'] = $matches[4];
|
||||
$info['isDir'] = strtoupper($matches[3]) === '<DIR>';
|
||||
$info['size'] = $info['isDir'] ? 0 : (int) $matches[3];
|
||||
$info['modified'] = strtotime($matches[1]);
|
||||
$info['created'] = $info['modified'];
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info in the raw list item for Unix
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
private static function parseUnixRawListString($rawListString)
|
||||
{
|
||||
//Regex below gets info from raw string on UNIX systems
|
||||
//Example: drwxr-xr-x 2 user group 4096 Jan 1 1970 folder
|
||||
//Groups: 1 2 3 4 5 6 7
|
||||
$regex = '/^([drwx\-]{10})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+\d{1,2}:\d{1,2}|\w{3}\s+\d{1,2}\s+\d{4})\s+(.+)$/';
|
||||
if (preg_match($regex, $rawListString, $matches) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = [];
|
||||
$info['name'] = $matches[7];
|
||||
$info['isDir'] = $matches[1][0] === 'd';
|
||||
$info['size'] = $info['isDir'] ? 0 : (int) $matches[5];
|
||||
$info['modified'] = strtotime($matches[6]);
|
||||
$info['created'] = $info['modified'];
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\FTPStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var FTPStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$server = $tplData["server"];
|
||||
/** @var int */
|
||||
$port = $tplData["port"];
|
||||
/** @var string */
|
||||
$username = $tplData["username"];
|
||||
/** @var string */
|
||||
$password = $tplData["password"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var int */
|
||||
$timeout = $tplData["timeout"];
|
||||
/** @var bool */
|
||||
$useCurl = $tplData["useCurl"];
|
||||
/** @var bool */
|
||||
$isPassive = $tplData["isPassive"];
|
||||
/** @var bool */
|
||||
$useSSL = $tplData["useSSL"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Credentials", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_server"><?php esc_html_e("Server", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_server"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_server"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($server); ?>"
|
||||
data-parsley-errors-container="#ftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
>
|
||||
<label for="ftp_server">
|
||||
<?php esc_html_e("Port", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<input
|
||||
name="ftp_port"
|
||||
id="ftp_port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
style="width:75px"
|
||||
value="<?php echo (int) $port; ?>"
|
||||
data-parsley-errors-container="#ftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-range="[1, 65535]"
|
||||
>
|
||||
<div id="ftp_server_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_username"><?php esc_html_e("Username", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_username"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_username"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($username); ?>"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_password"><?php esc_html_e("Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_password"
|
||||
name="ftp_password"
|
||||
type="password"
|
||||
class="dup-empty-field-on-submit"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_password2"><?php esc_html_e("Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#ftp_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#ftp_password"
|
||||
data-parsley-equalto-message="<?php esc_html_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="ftp_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Settings", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="_ftp_storage_folder"
|
||||
name="_ftp_storage_folder"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($storageFolder); ?>"
|
||||
>
|
||||
<p>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Folder where packages will be stored. This should be unique for each web-site using Duplicator.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_max_files"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="ftp_max_files">
|
||||
<input
|
||||
id="ftp_max_files"
|
||||
name="ftp_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#ftp_max_files_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-min="0"
|
||||
>
|
||||
<?php esc_html_e("Number of packages to keep in folder.", 'duplicator-pro'); ?> <br/>
|
||||
<i><?php esc_html_e("When this limit is exceeded, the oldest package will be deleted. Set to 0 for no limit. ", 'duplicator-pro'); ?></i>
|
||||
</label>
|
||||
<div id="ftp_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_timeout_in_secs"><?php esc_html_e("Timeout", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
|
||||
<label for="ftp_timeout_in_secs">
|
||||
<input
|
||||
id="ftp_timeout"
|
||||
name="ftp_timeout_in_secs"
|
||||
type="number"
|
||||
min="10"
|
||||
value="<?php echo (int) $timeout; ?>"
|
||||
data-parsley-errors-container="#ftp_timeout_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-min="10"
|
||||
>
|
||||
<label for="ftp_timeout_in_secs">
|
||||
<?php esc_html_e("seconds", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Do not modify this setting unless you know the expected result or have talked to support.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</label>
|
||||
<div id="ftp_timeout_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_ssl"><?php esc_html_e("Explicit SSL", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input name="_ftp_ssl" <?php checked($useSSL); ?> class="checkbox" value="1" type="checkbox" id="_ftp_ssl" >
|
||||
<label for="_ftp_ssl"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_passive_mode"><?php esc_html_e("Passive Mode", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
<?php checked($isPassive); ?>
|
||||
class="checkbox"
|
||||
value="1"
|
||||
type="checkbox"
|
||||
name="_ftp_passive_mode"
|
||||
id="_ftp_passive_mode"
|
||||
>
|
||||
<label for="_ftp_passive_mode"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_use_curl"><?php esc_html_e("cURL", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input <?php checked($useCurl); ?> class="checkbox" value="1" type="checkbox" name="_ftp_use_curl" id="_ftp_use_curl">
|
||||
<label for="_ftp_use_curl"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
<p><i><?php esc_html_e("PHP cURL. Only check if connection test recommends it.", 'duplicator-pro'); ?></i></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label> </label></th>
|
||||
<td>
|
||||
<p>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
__(
|
||||
"<b>Note:</b> This setting is for FTP and FTPS (FTP/SSL) only.
|
||||
To use SFTP (SSH File Transfer Protocol) change the type dropdown above.",
|
||||
'duplicator-pro'
|
||||
),
|
||||
array(
|
||||
'b' => array(),
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\SFTPStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var SFTPStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$server = $tplData["server"];
|
||||
/** @var int */
|
||||
$port = $tplData["port"];
|
||||
/** @var string */
|
||||
$username = $tplData["username"];
|
||||
/** @var string */
|
||||
$password = $tplData["password"];
|
||||
/** @var string */
|
||||
$privateKey = $tplData["privateKey"];
|
||||
/** @var string */
|
||||
$privateKeyPwd = $tplData["privateKeyPwd"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var int */
|
||||
$timeout = $tplData["timeout"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Credentials", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_server"><?php esc_html_e("Server", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input id="sftp_server" class="dup-empty-field-on-submit" name="sftp_server"
|
||||
data-parsley-errors-container="#sftp_server_error_container" type="text" a
|
||||
utocomplete="off" value="<?php echo esc_attr($server); ?>">
|
||||
<label for="sftp_server">
|
||||
<?php esc_html_e("Port", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<input
|
||||
name="sftp_port"
|
||||
id="sftp_port"
|
||||
data-parsley-errors-container="#sftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-range="[1, 65535]"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
style="width:75px"
|
||||
value="<?php echo (int) $port; ?>"
|
||||
>
|
||||
<div id="sftp_server_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_username"><?php esc_html_e("Username", 'duplicator-pro'); ?></label></th>
|
||||
<td><input id="sftp_username" class="dup-empty-field-on-submit" name="sftp_username"
|
||||
type="text" autocomplete="off" value="<?php echo esc_attr($username); ?>" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_password"><?php esc_html_e("Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_password"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_password"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_password2"><?php esc_html_e("Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#sftp_password"
|
||||
data-parsley-equalto-message="<?php esc_attr_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="sftp_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key"><?php esc_html_e("Private Key (PuTTY)", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_file"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_file"
|
||||
onchange="DuplicatorReadPrivateKey(this);"
|
||||
type="file"
|
||||
accept="ppk"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_error_container"
|
||||
><br/>
|
||||
<input type="hidden" name="sftp_private_key" id="sftp_private_key" value="<?php echo esc_attr($privateKey); ?>" />
|
||||
<div id="sftp_private_key_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key_password"><?php esc_html_e("Private Key Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_password"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_password"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($privateKeyPwd))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_password_error_container"
|
||||
>
|
||||
<br/>
|
||||
<div id="sftp_private_key_password_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key_password2"><?php esc_html_e("Private Key Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($privateKeyPwd))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#sftp_private_key_password"
|
||||
data-parsley-equalto-message="<?php esc_html_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="sftp_private_key_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Settings", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_sftp_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input id="_sftp_storage_folder" name="_sftp_storage_folder" type="text" value="<?php echo esc_attr($storageFolder); ?>">
|
||||
<p>
|
||||
<i>
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x(
|
||||
'Folder where packages will be stored. This should be %1$san absolute path, not a relative path%2$s
|
||||
and be unique for each web-site using Duplicator.',
|
||||
'%1$s representes the opening and %2$s the closing bold (<b>) tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<b>',
|
||||
'</b>'
|
||||
);
|
||||
?>
|
||||
</i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_max_files"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="sftp_max_files">
|
||||
<input id="sftp_max_files" name="sftp_max_files" data-parsley-errors-container="#sftp_max_files_error_container"
|
||||
type="text" value="<?php echo (int) $maxPackages; ?>">
|
||||
<?php esc_html_e("Number of packages to keep in folder.", 'duplicator-pro'); ?> <br/>
|
||||
<i><?php esc_html_e("When this limit is exceeded, the oldest package will be deleted. Set to 0 for no limit.", 'duplicator-pro'); ?></i>
|
||||
</label>
|
||||
<div id="sftp_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_timeout_in_secs"><?php esc_html_e("Timeout", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="sftp_timeout_in_secs">
|
||||
<input
|
||||
id="sftp_timeout"
|
||||
name="sftp_timeout_in_secs"
|
||||
data-parsley-errors-container="#sftp_timeout_error_container"
|
||||
type="text"
|
||||
value="<?php echo (int) $timeout; ?>"
|
||||
>
|
||||
<label for="sftp_timeout_in_secs">
|
||||
<?php esc_html_e("seconds", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Do not modify this setting unless you know the expected result or have talked to support.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</label>
|
||||
<div id="sftp_timeout_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
<script>
|
||||
jQuery(document).ready(function ($) {
|
||||
DuplicatorReadPrivateKey = function (file_obj)
|
||||
{
|
||||
var files = file_obj.files;
|
||||
var private_key = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
$("#sftp_private_key").val(e.target.result);
|
||||
}
|
||||
reader.readAsText(private_key);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user