first commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DUPLICATOR CLOUD ADDON
|
||||
*
|
||||
* Name: Duplicator PRO DupCloud Storage
|
||||
* Version: 1
|
||||
* Author: Duplicator
|
||||
* Author URI: https://duplicator.com/
|
||||
*
|
||||
* PHP version 5.6
|
||||
*
|
||||
* @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\DupCloudAddon;
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
|
||||
use Duplicator\Addons\DupCloudAddon\Ajax\ServicesDupCloud;
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudRateLimitHandler;
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Controllers\StoragePageController;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Core\Bootstrap;
|
||||
use Duplicator\Core\CapMng;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Package\AbstractPackage;
|
||||
use Duplicator\Package\Storage\UploadInfo;
|
||||
use Duplicator\Utils\Logging\DupLog;
|
||||
use Duplicator\Views\AdminNotices;
|
||||
use Exception;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Duplicator Cloud Storage addon class
|
||||
*/
|
||||
class DupCloudAddon extends AbstractAddonCore
|
||||
{
|
||||
const OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED = 'dupli_opt_dup_cloud_out_of_space_notice';
|
||||
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
if (!defined('DUPLICATOR_CLOUD_HOST')) {
|
||||
define('DUPLICATOR_CLOUD_HOST', 'https://cloud.duplicator.com');
|
||||
}
|
||||
add_action('duplicator_register_storage_types', [$this, 'registerStorages']);
|
||||
add_filter('duplicator_template_file', [self::class, 'getTemplateFile'], 10, 2);
|
||||
add_filter('duplicator_admin_notices', [self::class, 'adminNotices']);
|
||||
add_action('admin_init', [self::class, 'registerJsCss']);
|
||||
add_action('duplicator_settings_general_before', [self::class, 'renderLicenseContent'], 20);
|
||||
add_action('duplicator_transfer_cancelled', [self::class, 'cancelUpload'], 10, 1);
|
||||
add_action('duplicator_transfer_failed', [self::class, 'markUploadAsFailed'], 10, 1);
|
||||
add_filter('duplicator_validate_upload_info_data', [self::class, 'validateUploadInfoData'], 10, 4);
|
||||
|
||||
add_action('duplicator_backups_page_header_after', [self::class, 'backupsPageHeaderAfter'], 10, 0);
|
||||
|
||||
DupCloudRateLimitHandler::init();
|
||||
|
||||
(new ServicesDupCloud())->init();
|
||||
QuickConnect::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Before add upload info hook
|
||||
*
|
||||
* @param WP_Error $errors errors
|
||||
* @param AbstractPackage $package package
|
||||
* @param int $storageId storage id
|
||||
* @param bool $isDownload is download
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
public static function validateUploadInfoData(WP_Error $errors, AbstractPackage $package, int $storageId, bool $isDownload): WP_Error
|
||||
{
|
||||
if (($storage = AbstractStorageEntity::getById($storageId)) === false) {
|
||||
$errors->add('storage_id', sprintf(__('Could not find storage ID %d!', 'duplicator-pro'), $storageId));
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if (
|
||||
$storage->getSType() === DupCloudStorage::getSType() &&
|
||||
in_array($storageId, $package->getValidStorages(true, 'id')) &&
|
||||
$isDownload === false
|
||||
) {
|
||||
$errors->add('storage_id', __('The backup you are trying to transfer already exists in the cloud storage.', 'duplicator-pro'));
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark upload as failed
|
||||
*
|
||||
* @param UploadInfo $uploadInfo Upload info
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function markUploadAsFailed(UploadInfo $uploadInfo): void
|
||||
{
|
||||
try {
|
||||
/** @var DupCloudStorage $storage */
|
||||
$storage = $uploadInfo->getStorage();
|
||||
if (!($storage instanceof DupCloudStorage)) {
|
||||
DupLog::infoTrace("Can't fail upload, storage not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($uploadInfo->isDownloadFromRemote()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$storage->failUpload($uploadInfo)) {
|
||||
throw new Exception("Failed to fail upload");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, "Can't fail upload");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display cloud manage button on backups page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function backupsPageHeaderAfter(): void
|
||||
{
|
||||
if (!CapMng::getInstance()->can(CapMng::CAP_STORAGE, false)) {
|
||||
return;
|
||||
}
|
||||
TplMng::getInstance()->render('dupcloudaddon/parts/backups_header_button');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel upload
|
||||
*
|
||||
* @param UploadInfo $uploadInfo Upload info
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cancelUpload(UploadInfo $uploadInfo): void
|
||||
{
|
||||
try {
|
||||
/** @var DupCloudStorage $storage */
|
||||
$storage = $uploadInfo->getStorage();
|
||||
if (!($storage instanceof DupCloudStorage)) {
|
||||
DupLog::infoTrace("Can't cancel upload, storage not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($uploadInfo->isDownloadFromRemote()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$storage->cancelUpload($uploadInfo)) {
|
||||
throw new Exception("Failed to cancel upload");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, "Can't cancel upload");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render page content
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function renderLicenseContent(): void
|
||||
{
|
||||
if (!CapMng::getInstance()->can(CapMng::CAP_LICENSE, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CapMng::getInstance()->can(CapMng::CAP_STORAGE, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
TplMng::getInstance()->render(
|
||||
'dupcloudaddon/configs/general_settings',
|
||||
[
|
||||
'storage' => DupCloudStorage::getUniqueStorage(),
|
||||
'auto_activate_storage' => LicensingController::isActivationLicenseRender(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add notice to admin notices
|
||||
*
|
||||
* @param callable[] $notices Admin notices
|
||||
*
|
||||
* @return callable[] Admin notices
|
||||
*/
|
||||
public static function adminNotices(array $notices): array
|
||||
{
|
||||
$notices[] = [
|
||||
self::class,
|
||||
'dupCloudOutOfSpaceNotice',
|
||||
];
|
||||
|
||||
$notices[] = [
|
||||
self::class,
|
||||
'dupCloudRateLimitNotice',
|
||||
];
|
||||
|
||||
return $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notice in case we were enable to fetch contents of S3 bucket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function dupCloudRateLimitNotice(): void
|
||||
{
|
||||
if (
|
||||
!ControllersManager::getInstance()->isDuplicatorPage() ||
|
||||
!CapMng::can(CapMng::CAP_STORAGE, false) ||
|
||||
!DupCloudRateLimitHandler::hasRateLimitError()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
_x(
|
||||
'You made too many requests to Duplicator Cloud. Please try again in %1$s seconds.',
|
||||
'1: Time in seconds',
|
||||
'duplicator-pro'
|
||||
),
|
||||
DupCloudRateLimitHandler::retryAfter()
|
||||
);
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
$message,
|
||||
AdminNotices::GEN_ERROR_NOTICE,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notice in case we were enable to fetch contents of S3 bucket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function dupCloudOutOfSpaceNotice(): void
|
||||
{
|
||||
if (get_option(self::OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED, false) == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ControllersManager::getInstance()->isDuplicatorPage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CapMng::can(CapMng::CAP_STORAGE, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var DupCloudStorage[] $dupCloudStorages */
|
||||
$dupCloudStorages = AbstractStorageEntity::getAllBySType(DupCloudStorage::getSType());
|
||||
if (count($dupCloudStorages) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dupCloudStorage = $dupCloudStorages[0];
|
||||
if (!$dupCloudStorage->isAuthorized()) {
|
||||
//Only show message if storage is authorized and out of space
|
||||
return;
|
||||
}
|
||||
|
||||
if ($dupCloudStorage->getFreeSpace() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storageEditUrl = StoragePageController::getEditUrl($dupCloudStorage);
|
||||
|
||||
$message = wp_kses(
|
||||
sprintf(
|
||||
_x(
|
||||
'The Duplicator Cloud storage is out of space. Please make sure you have enough
|
||||
space available in the %1$s%3$s%2$s storage location.',
|
||||
'1: open link tag, 2: close link tag, 3: storage name',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="' . esc_url($storageEditUrl) . '" target="_blank">',
|
||||
'</a>',
|
||||
esc_html($dupCloudStorage->getName())
|
||||
),
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
'br' => [],
|
||||
]
|
||||
);
|
||||
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
$message,
|
||||
AdminNotices::GEN_ERROR_NOTICE,
|
||||
true,
|
||||
['dupli-quick-fix-notice'],
|
||||
[
|
||||
'data-to-dismiss' => self::OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerStorages(): void
|
||||
{
|
||||
DupCloudStorage::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, 'dupcloudaddon/') === 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_dup_cloud_count'] = 0;
|
||||
|
||||
foreach ($storages as $storage) {
|
||||
if ($storage->getStype() === DupCloudStorage::getSType()) {
|
||||
$storageNums['storages_dup_cloud_count']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register styles and scripts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerJsCss(): void
|
||||
{
|
||||
if (wp_doing_ajax()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$min = Bootstrap::getMinPrefix();
|
||||
wp_register_style(
|
||||
'dup-addon-dupcloud-addon',
|
||||
self::getAddonUrl() . "/assets/css/dupcloudaddon{$min}.css",
|
||||
['dup-plugin-global-style'],
|
||||
DUPLICATOR_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_style('dup-addon-dupcloud-addon');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath(): string
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile(): string
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
|
||||
!*** css ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.100.2/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].use[1]!./node_modules/.pnpm/postcss-loader@8.2.0_postcss@8.5.6_webpack@5.100.2/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./node_modules/.pnpm/sass-loader@16.0.5_sass@1.89.2_webpack@5.100.2/node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[1].use[3]!./addons/dupcloudaddon/assets/sass/main.scss ***!
|
||||
\***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
|
||||
/*!
|
||||
DUPLICATOR DUP CLOUD ADDON STYLE
|
||||
*/
|
||||
/**
|
||||
* Foundation for Sites
|
||||
* Version 6.9.0
|
||||
* https://get.foundation
|
||||
* Licensed under MIT Open Source
|
||||
*/
|
||||
.dup-styles .dup-box.dup-cloud-connect {
|
||||
width: 600px;
|
||||
background: #fefefe;
|
||||
border: 1px solid #1D2327;
|
||||
}
|
||||
.dup-styles .dup-box.dup-cloud-connect .dup-box-title {
|
||||
background: rgb(240, 240, 241);
|
||||
}
|
||||
.dup-styles .dup-box.dup-cloud-connect .dupli-admin-notice {
|
||||
width: 100%;
|
||||
background: rgb(240, 240, 241);
|
||||
}
|
||||
.dup-styles .dup-box.dup-cloud-connect .dup-storage-info {
|
||||
background-color: rgb(240, 240, 241);
|
||||
border: 1px solid #c0c0c0;
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 10px;
|
||||
}
|
||||
.dup-styles .dup-box.dup-cloud-connect input {
|
||||
width: 100%;
|
||||
}
|
||||
1
wp-content/plugins/duplicator-pro/addons/dupcloudaddon/assets/css/dupcloudaddon.min.css
vendored
Normal file
1
wp-content/plugins/duplicator-pro/addons/dupcloudaddon/assets/css/dupcloudaddon.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.dup-styles .dup-box.dup-cloud-connect{background:#fefefe;border:1px solid #1d2327;width:600px}.dup-styles .dup-box.dup-cloud-connect .dup-box-title{background:#f0f0f1}.dup-styles .dup-box.dup-cloud-connect .dupli-admin-notice{background:#f0f0f1;width:100%}.dup-styles .dup-box.dup-cloud-connect .dup-storage-info{background-color:#f0f0f1;border:1px solid silver;margin-bottom:1.25rem;padding:10px}.dup-styles .dup-box.dup-cloud-connect input{width:100%}
|
||||
@@ -0,0 +1,25 @@
|
||||
.dup-box.dup-cloud-connect {
|
||||
width: 600px;
|
||||
background: $white;
|
||||
border: 1px solid $black;
|
||||
|
||||
.dup-box-title {
|
||||
background: $body-background;
|
||||
}
|
||||
|
||||
.dupli-admin-notice {
|
||||
width: 100%;
|
||||
background: $body-background;
|
||||
}
|
||||
|
||||
.dup-storage-info {
|
||||
background-color: $body-background;
|
||||
border: 1px solid $medium-gray;
|
||||
margin-bottom: $global-margin;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*!
|
||||
DUPLICATOR DUP CLOUD ADDON STYLE
|
||||
*/
|
||||
|
||||
@import "../../../../node_modules/foundation-sites/scss/foundation";
|
||||
@import '../../../../assets/sass/settings';
|
||||
|
||||
.dup-styles {
|
||||
@import "components/cloud_connect";
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DupCloud ajax services
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Ajax;
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
|
||||
use Duplicator\Ajax\AbstractAjaxService;
|
||||
use Duplicator\Ajax\AjaxWrapper;
|
||||
use Duplicator\Core\CapMng;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
|
||||
/**
|
||||
* Handles connection requests for DupCloud
|
||||
*/
|
||||
class ServicesDupCloud extends AbstractAjaxService
|
||||
{
|
||||
const AJAX_ACTION_QUICK_CONNECT = 'duplicator_dupcloud_quick_connect';
|
||||
|
||||
/**
|
||||
* Init ajax calls
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->addAjaxCall('wp_ajax_' . self::AJAX_ACTION_QUICK_CONNECT, 'getStorageTokens');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage tokens via license
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getStorageTokens(): void
|
||||
{
|
||||
AjaxWrapper::json(
|
||||
[
|
||||
$this,
|
||||
'getStorageTokensCallback',
|
||||
],
|
||||
self::AJAX_ACTION_QUICK_CONNECT,
|
||||
SnapUtil::sanitizeStrictInput(INPUT_POST, 'nonce'),
|
||||
CapMng::CAP_STORAGE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage tokens callback
|
||||
*
|
||||
* @return array{tokens:array<string,mixed>,html:string}
|
||||
*/
|
||||
public function getStorageTokensCallback(): array
|
||||
{
|
||||
$tokens = QuickConnect::getStorageTokens();
|
||||
|
||||
return [
|
||||
'tokens' => $tokens,
|
||||
'html' => TplMng::getInstance()->render(
|
||||
'dupcloudaddon/connect/quick_connect',
|
||||
['tokens' => $tokens],
|
||||
false
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when a presigned URL has expired
|
||||
*/
|
||||
class PresignedUrlExpiredException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,897 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Models;
|
||||
|
||||
use Duplicator\Models\DynamicGlobalEntity;
|
||||
use Duplicator\Models\GlobalEntity;
|
||||
use Duplicator\Utils\Logging\DupLog;
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudStorageAdapter;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
use Duplicator\Core\UniqueId;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Installer\Package\ArchiveDescriptor;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Models\Storages\StorageAuthInterface;
|
||||
use Duplicator\Package\AbstractPackage;
|
||||
use Duplicator\Package\Create\BuildComponents;
|
||||
use Duplicator\Package\PackageUtils;
|
||||
use Duplicator\Package\Recovery\BackupPackage;
|
||||
use Duplicator\Package\Recovery\RecoveryStatus;
|
||||
use Duplicator\Package\Storage\UploadInfo;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @property ?DupCloudStorageAdapter $adapter
|
||||
*
|
||||
* @phpstan-import-type IneligibilityReasons from RecoveryStatus
|
||||
*/
|
||||
class DupCloudStorage extends AbstractStorageEntity implements StorageAuthInterface
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
|
||||
/** @var int */
|
||||
const UPLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
|
||||
/** @var int */
|
||||
const UPLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
|
||||
/** @var int */
|
||||
const DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
|
||||
/** @var int */
|
||||
const DOWNLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
|
||||
/** @var int */
|
||||
const DOWNLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
|
||||
|
||||
/** @var array<string, array{result:bool,reasons:IneligibilityReasons}> */
|
||||
private static $eligibilityCache = [];
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->name = __('Duplicator Cloud', "duplicator-pro");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority(): int
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an upload as failed using the upload info
|
||||
*
|
||||
* @param UploadInfo $uploadInfo Upload info
|
||||
*
|
||||
* @return bool True if success, false otherwise
|
||||
*/
|
||||
public function failUpload(UploadInfo $uploadInfo)
|
||||
{
|
||||
if (isset($uploadInfo->copyExtraData['UploadUuid'])) {
|
||||
return $this->getAdapter()->failUpload($uploadInfo->copyExtraData['UploadUuid']);
|
||||
}
|
||||
|
||||
$archiveName = $uploadInfo->generalExtraData['backup_details']['file_info']['backup_filename'] ?? '';
|
||||
if (strlen($archiveName) > 0) {
|
||||
return $this->getAdapter()->failUploadByName($archiveName);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an upload using the upload info
|
||||
*
|
||||
* @param UploadInfo $uploadInfo Upload info
|
||||
*
|
||||
* @return bool True if success, false otherwise
|
||||
*/
|
||||
public function cancelUpload(UploadInfo $uploadInfo)
|
||||
{
|
||||
if (isset($uploadInfo->copyExtraData['UploadUuid'])) {
|
||||
return $this->getAdapter()->cancelUpload($uploadInfo->copyExtraData['UploadUuid']);
|
||||
}
|
||||
|
||||
$archiveName = $uploadInfo->generalExtraData['backup_details']['file_info']['backup_filename'] ?? '';
|
||||
if (strlen($archiveName) > 0) {
|
||||
return $this->getAdapter()->cancelUploadByName($archiveName);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new storage object by type
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
protected static function getNewStorageInstance(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig(): array
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
return array_merge(
|
||||
$config,
|
||||
[
|
||||
'accessToken' => '',
|
||||
'userName' => '',
|
||||
'userEmail' => '',
|
||||
'totalSpace' => 0,
|
||||
'freeSpace' => 0,
|
||||
'authorized' => false,
|
||||
'websiteUuid' => '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Storages test
|
||||
*
|
||||
* @param string $message Test message
|
||||
*
|
||||
* @return bool return true if success, false otherwise
|
||||
*/
|
||||
public function test(string &$message = ''): bool
|
||||
{
|
||||
try {
|
||||
$this->testLog->reset();
|
||||
$message = sprintf(__('Testing %s storage...', 'duplicator-pro'), static::getStypeName());
|
||||
$this->testLog->addMessage($message);
|
||||
|
||||
if (static::isSupported() == false) {
|
||||
$message = sprintf(__('Storage %s isn\'t supported on current server', 'duplicator-pro'), static::getStypeName());
|
||||
$this->testLog->addMessage($message);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->testLog->addMessage(__('Check if storage is ready to use.', 'duplicator-pro'));
|
||||
$validMessage = '';
|
||||
$adapter = $this->getAdapter();
|
||||
if ($adapter->isValid($validMessage) == false) {
|
||||
$message = sprintf(
|
||||
__('Storage %1$s is not valid message %2$s', 'duplicator-pro'),
|
||||
static::getStypeName(),
|
||||
$validMessage
|
||||
);
|
||||
$this->testLog->addMessage($message);
|
||||
return false;
|
||||
}
|
||||
$this->testLog->addMessage(__('Successfully storage test.', 'duplicator-pro'));
|
||||
$message = __('Successfully storage test.', 'duplicator-pro');
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
$this->testLog->addMessage(sprintf(__('Error during storage test: %s', 'duplicator-pro'), $message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authorzation url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAuthUrl(): string
|
||||
{
|
||||
return DupCloudClient::getAuthUrl(home_url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType(): int
|
||||
{
|
||||
return 17;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon URL
|
||||
*
|
||||
* @return string Returns the storage icon URL
|
||||
*/
|
||||
public static function getStypeIconURL(): string
|
||||
{
|
||||
return DUPLICATOR_IMG_URL . '/duplicator-logo-icon.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName(): string
|
||||
{
|
||||
return __('Duplicator Cloud', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString(): string
|
||||
{
|
||||
return 'Manage Backups';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL to backup list for website
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBackupsUrl(): string
|
||||
{
|
||||
return SnapIO::trailingslashit(DupCloudClient::manageWebsitesUrl()) . $this->config['websiteUuid'] . '/backups';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html anchor tag of location
|
||||
*
|
||||
* @return string Returns an html anchor tag with the storage location as a hyperlink.
|
||||
*/
|
||||
public function getHtmlLocationLink(): string
|
||||
{
|
||||
if ($this->isAuthorized()) {
|
||||
$websitesUrl = $this->getBackupsUrl();
|
||||
return '<a href="' . esc_url($websitesUrl) . '" target="_blank" >' .
|
||||
esc_html__('Manage Backups', 'duplicator-pro') .
|
||||
'</a>';
|
||||
} else {
|
||||
return '#';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is unique, if true only one storage of this type can exist
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUnique(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a grid break should be inserted after this storage type in the selector UI
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isGridBreakAfter(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @param ?string $errorMsg Reference to store error message
|
||||
* @param bool $force Force the storage to be revalidated
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid(?string &$errorMsg = '', bool $force = false): bool
|
||||
{
|
||||
$adapter = $this->getAdapter();
|
||||
$isValid = $adapter->isValid($errorMsg, $force);
|
||||
|
||||
// 'isValid()' can be false when there is no free space
|
||||
// 'authorized' determines if the info should be considered for saving
|
||||
if ($force === true && $adapter->isAuthorized()) {
|
||||
$data = [
|
||||
'authorized' => $adapter->isAuthorized(),
|
||||
'userName' => $adapter->getUserName(),
|
||||
'userEmail' => $adapter->getUserEmail(),
|
||||
'totalSpace' => $adapter->getTotalSpace(),
|
||||
'freeSpace' => $adapter->getFreeSpace(),
|
||||
'websiteUuid' => $adapter->getWebsiteUuid(),
|
||||
];
|
||||
|
||||
if ($this->hasConfigChanged($data)) {
|
||||
$this->config = array_merge($this->config, $data);
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is autorized
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthorized(): bool
|
||||
{
|
||||
return $this->config['authorized'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize
|
||||
*
|
||||
* @param string $token Token
|
||||
*
|
||||
* @return bool True if authorized, false if failed
|
||||
*/
|
||||
public function authorize(string $token): bool
|
||||
{
|
||||
$adapter = new DupCloudStorageAdapter($token);
|
||||
|
||||
$userInfo = $adapter->getUserInfo();
|
||||
|
||||
$this->config['accessToken'] = $token;
|
||||
$this->config['userName'] = $userInfo['name'];
|
||||
$this->config['userEmail'] = $userInfo['email'];
|
||||
$this->config['authorized'] = true;
|
||||
$this->config['websiteUuid'] = '';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized from HTTP request
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if authorized, false if failed
|
||||
*/
|
||||
public function authorizeFromRequest(&$message = ''): bool
|
||||
{
|
||||
// Allow pipe character in the token for compound tokens
|
||||
if (($accessToken = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'access_token', '')) === '') {
|
||||
DupLog::trace('No access token found');
|
||||
$message = __('No access token provided', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->name = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'name', '');
|
||||
$this->notes = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'notes', '');
|
||||
|
||||
try {
|
||||
// Check if this is a compound token (new method) or direct token (legacy)
|
||||
if (strpos($accessToken, '.') !== false && strpos($accessToken, '|') !== false) {
|
||||
// New compound token authentication (contains both . and |)
|
||||
$client = new DupCloudClient();
|
||||
|
||||
// Get site identifier
|
||||
$siteIdentifier = UniqueId::getInstance()->getIdentifier();
|
||||
|
||||
DupLog::trace('Authenticating with compound token for site: ' . $siteIdentifier);
|
||||
|
||||
// Authenticate using compound token
|
||||
$authResult = $client->authenticateSite($accessToken, $siteIdentifier);
|
||||
|
||||
// Store the permanent token and all data directly
|
||||
$this->config['accessToken'] = $authResult['token'];
|
||||
$this->config['authorized'] = true;
|
||||
$this->config['userName'] = $authResult['user_name'] ?? '';
|
||||
$this->config['userEmail'] = $authResult['user_email'] ?? '';
|
||||
$this->config['totalSpace'] = $authResult['total_space'] ?? 0;
|
||||
$this->config['freeSpace'] = $authResult['free_space'] ?? 0;
|
||||
|
||||
DupLog::trace('Website connected: ' . ($authResult['name'] ?? ''));
|
||||
DupLog::trace('Storage info - Total: ' . $this->config['totalSpace'] . ', Free: ' . $this->config['freeSpace']);
|
||||
DupLog::trace('User info - Name: ' . $this->config['userName'] . ', Email: ' . $this->config['userEmail']);
|
||||
|
||||
$currentPage = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'current_page', '');
|
||||
$storagePageUrl = ControllersManager::getInstance()->getMenuLink(
|
||||
ControllersManager::STORAGE_SUBMENU_SLUG
|
||||
);
|
||||
|
||||
$message = TplMng::getInstance()->render(
|
||||
'admin_pages/storages/parts/auth_success_message',
|
||||
[
|
||||
'storagePageUrl' => $storagePageUrl,
|
||||
'storageName' => $this->getStypeName(),
|
||||
'isSettingsPage' => $currentPage === ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
],
|
||||
false
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
// Legacy method - direct token
|
||||
return $this->authorize($accessToken);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DupLog::trace('Authorization failed: ' . $e->getMessage());
|
||||
$message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes authorization
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if revoked, false if failed
|
||||
*/
|
||||
public function revokeAuthorization(&$message = ''): bool
|
||||
{
|
||||
try {
|
||||
if (!$this->isAuthorized()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->getAdapter()->revokeAuthorization()) {
|
||||
throw new Exception(__('Error revoking authorization.', 'duplicator-pro'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DupLog::traceException($e, 'REVOKE AUTHORIZATION ERROR BUT COUNTINUE RESETTING CONFIG');
|
||||
$message = $e->getMessage();
|
||||
return false;
|
||||
} finally {
|
||||
$this->config = static::getDefaultConfig();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
$message = __('Duplicator Cloud is disconnected successfully.', 'duplicator-pro');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the config fields template path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getConfigFieldsTemplatePath(): string
|
||||
{
|
||||
return 'dupcloudaddon/configs/dupcloud';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the config fields template data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getConfigFieldsData(): array
|
||||
{
|
||||
return $this->getDefaultConfigFieldsData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default config fields template data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getDefaultConfigFieldsData(): array
|
||||
{
|
||||
return [
|
||||
'storage' => $this,
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'userName' => $this->config['userName'],
|
||||
'userEmail' => $this->config['userEmail'],
|
||||
'totalSpace' => $this->config['totalSpace'],
|
||||
'freeSpace' => $this->config['freeSpace'],
|
||||
'authorized' => $this->config['authorized'],
|
||||
'websiteUuid' => $this->config['websiteUuid'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = ''): bool
|
||||
{
|
||||
if ((parent::updateFromHttpRequest($message) === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'dupcloud_max_files', 10);
|
||||
|
||||
$message = __('Dupicator Cloud Storage was updated.', 'duplicator-pro');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage adapter
|
||||
*
|
||||
* @return DupCloudStorageAdapter
|
||||
*/
|
||||
protected function getAdapter(): DupCloudStorageAdapter
|
||||
{
|
||||
if ($this->adapter === null) {
|
||||
$this->adapter = new DupCloudStorageAdapter(
|
||||
$this->config['accessToken'],
|
||||
DupCloudClient::BACKUP_TYPE_STANDARD,
|
||||
$this->config['max_packages']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get general data for backup
|
||||
*
|
||||
* @param AbstractPackage $package the Backup
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected function getGeneralExtraData(AbstractPackage $package): array
|
||||
{
|
||||
return array_merge(
|
||||
parent::getGeneralExtraData($package),
|
||||
['backup_details' => self::getBackupDetails($package) ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Backup details
|
||||
*
|
||||
* @param AbstractPackage $package the Backup
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getBackupDetails(AbstractPackage $package): array
|
||||
{
|
||||
$data = [
|
||||
'system' => [
|
||||
'php_version' => $package->VersionPHP,
|
||||
'wp_version' => $package->VersionWP,
|
||||
'plugin_type' => 'pro',
|
||||
'plugin_version' => $package->getVersion(),
|
||||
],
|
||||
'file_info' => [
|
||||
'backup_filename' => $package->getArchiveFilename(), // full file name
|
||||
'backup_name' => $package->getName(), // name, first part of file name
|
||||
'backup_hash' => $package->getHash(), // hash [a-z0-9]{20}_[0-9]{14}
|
||||
'created' => $package->getCreated(),
|
||||
'type' => PackageUtils::getExecTypeString($package->getExecutionType(), $package->template_id),
|
||||
'engine' => PackageUtils::getEngineTypeString(
|
||||
$package->build_progress->current_build_mode,
|
||||
$package->ziparchive_mode
|
||||
),
|
||||
'secure_mode' => $package->Installer->OptsSecureOn,
|
||||
'runtime' => $package->Runtime,
|
||||
'notes' => $package->notes,
|
||||
'installer' => $package->Installer->getInstallerName(),
|
||||
'backup_size_from_plugin' => $package->Archive->Size, // Used on upload start to know the estimated file size
|
||||
'filters' => [
|
||||
'enabled' => $package->Archive->FilterOn,
|
||||
'directories' => [
|
||||
'user' => $package->Archive->FilterInfo->Dirs->Instance,
|
||||
'unreadable' => $package->Archive->FilterInfo->Dirs->Unreadable,
|
||||
],
|
||||
'files' => [
|
||||
'user' => $package->Archive->FilterInfo->Files->Instance,
|
||||
'unreadable' => $package->Archive->FilterInfo->Files->Unreadable,
|
||||
],
|
||||
'extensions' => $package->Archive->FilterInfo->Exts->Instance,
|
||||
],
|
||||
'components' => array_map(fn($component): string => BuildComponents::getLabel($component), $package->components),
|
||||
],
|
||||
'db_info' => [
|
||||
'engine' => $package->Database->info->dbEngine,
|
||||
'version' => $package->VersionDB,
|
||||
'name' => $package->Database->info->name,
|
||||
'size' => $package->Database->info->tablesSizeOnDisk,
|
||||
'filters' => [
|
||||
'enabled' => $package->Database->FilterOn,
|
||||
'tables' => explode(',', $package->Database->FilterTables),
|
||||
],
|
||||
'collations' => $package->Database->info->collationList,
|
||||
],
|
||||
];
|
||||
|
||||
$reasons = [];
|
||||
$isEligible = self::isCloudRecoveryEligible($package, $reasons);
|
||||
|
||||
$data['file_info']['installer_params'] = PackageUtils::getOverwriteParamFileName($package->getPrimaryInternalHash());
|
||||
$data['file_info']['is_recovery_eligible'] = $isEligible;
|
||||
$data['file_info']['recovery_ineligibility_reasons'] = $reasons;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a package is eligible to be a cloud recovery point.
|
||||
*
|
||||
* This checks recovery eligibility without requiring local storage,
|
||||
* since the package is being uploaded to cloud storage.
|
||||
*
|
||||
* When $reasons is passed, it is populated with the structured ineligibility data
|
||||
* (keys present only when the condition causes ineligibility).
|
||||
*
|
||||
* @param AbstractPackage $package the Backup
|
||||
* @param IneligibilityReasons $reasons Populated by reference with ineligibility data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function isCloudRecoveryEligible(AbstractPackage $package, array &$reasons = []): bool
|
||||
{
|
||||
$cacheKey = $package->getPrimaryInternalHash();
|
||||
if (!isset(self::$eligibilityCache[$cacheKey])) {
|
||||
$cacheReasons = [];
|
||||
self::$eligibilityCache[$cacheKey] = [
|
||||
'result' => (new RecoveryStatus($package))->meetsRecoveryRequirements($cacheReasons),
|
||||
'reasons' => $cacheReasons,
|
||||
];
|
||||
}
|
||||
|
||||
$reasons = self::$eligibilityCache[$cacheKey]['reasons'];
|
||||
return self::$eligibilityCache[$cacheKey]['result'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Backup transfer files
|
||||
*
|
||||
* Overrides parent to add the restore config file required by the cloud installer for any backup.
|
||||
*
|
||||
* @param AbstractPackage $package the Backup
|
||||
*
|
||||
* @return array<string,string> return array from => to
|
||||
*/
|
||||
protected function getPackageUploadFiles(AbstractPackage $package): array
|
||||
{
|
||||
$files = parent::getPackageUploadFiles($package);
|
||||
|
||||
$configPath = $this->createRestoreConfigFile($package);
|
||||
if ($configPath !== false) {
|
||||
// All direct files must be uploaded before the archive, order important
|
||||
$files = array_merge([$configPath => basename($configPath)], $files);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create temporary restore config file
|
||||
*
|
||||
* @param AbstractPackage $package the Backup
|
||||
*
|
||||
* @return string|false Path to temp file or false on failure
|
||||
*/
|
||||
protected function createRestoreConfigFile(AbstractPackage $package)
|
||||
{
|
||||
try {
|
||||
$archivePath = $package->getLocalPackageFilePath(AbstractPackage::FILE_TYPE_ARCHIVE);
|
||||
if ($archivePath === false || !file_exists($archivePath)) {
|
||||
throw new Exception('Archive file not found');
|
||||
}
|
||||
|
||||
$params = (new BackupPackage($archivePath, $package))->getOverwriteParams();
|
||||
return PackageUtils::writeOverwriteParams(DUPLICATOR_SSDIR_PATH_TMP, $package->getPrimaryInternalHash(), $params);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTrace('Failed to create restore Backup config file: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout(): int
|
||||
{
|
||||
$global = GlobalEntity::getInstance();
|
||||
return (int) ($global->php_max_worker_time_in_sec <= 0 ? 0 : $global->php_max_worker_time_in_sec * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicator cloud storage handles the pruning of old backups automatically
|
||||
*
|
||||
* @param array<string> $exclude List of Backups to exclude from deletion
|
||||
*
|
||||
* @return false|string[] false on failure or array of deleted files of Backups
|
||||
*/
|
||||
public function purgeOldPackages(array $exclude = [])
|
||||
{
|
||||
DupLog::infoTrace("Old backups are purged automatically by Duplicator Cloud storage");
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize(): int
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
return $dGlobal->getValInt(
|
||||
'dupcloud_upload_chunk_size_in_kb',
|
||||
self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
|
||||
) * KB_IN_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getDownloadChunkSize(): int
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
return $dGlobal->getValInt(
|
||||
'dupcloud_download_chunk_size_in_kb',
|
||||
self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
|
||||
) * KB_IN_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user name
|
||||
*
|
||||
* @return string Return user name or false if not available
|
||||
*/
|
||||
public function getUserName(): string
|
||||
{
|
||||
return $this->config['userName'] ?? __('unknown', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email
|
||||
*
|
||||
* @return string Return user email or false if not available
|
||||
*/
|
||||
public function getUserEmail(): string
|
||||
{
|
||||
return $this->config['userEmail'] ?? __('unknown', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total space
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalSpace(): int
|
||||
{
|
||||
return $this->config['totalSpace'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get free space
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUsedSpace(): int
|
||||
{
|
||||
$freeSpace = $this->config['freeSpace'] ?? 0;
|
||||
$totalSpace = $this->config['totalSpace'] ?? 0;
|
||||
|
||||
return $totalSpace - $freeSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get free space
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFreeSpace(): int
|
||||
{
|
||||
return $this->config['freeSpace'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get website UUID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWebsiteUuid(): string
|
||||
{
|
||||
return $this->config['websiteUuid'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique DupCloud storasge or new tempalte if not exists
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function getUniqueStorage(): self
|
||||
{
|
||||
$storages = static::getAllBySType(self::getSType());
|
||||
if (is_array($storages) && count($storages) > 0) {
|
||||
return $storages[0];
|
||||
} else {
|
||||
$storage = new self();
|
||||
return $storage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerType(): void
|
||||
{
|
||||
parent::registerType();
|
||||
|
||||
add_action('duplicator_update_global_storage_settings', function (): void {
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
|
||||
foreach (static::getDefaultSettings() as $key => $default) {
|
||||
$value = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, $key, $default);
|
||||
$dGlobal->setValInt($key, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
protected static function getDefaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'dupcloud_upload_chunk_size_in_kb' => self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB,
|
||||
'dupcloud_download_chunk_size_in_kb' => self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render global options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function renderGlobalOptions(): void
|
||||
{
|
||||
if (static::isHidden()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
TplMng::getInstance()->render(
|
||||
'dupcloudaddon/configs/global_options',
|
||||
[
|
||||
'uploadChunkSizeInKb' => $dGlobal->getValInt(
|
||||
'dupcloud_upload_chunk_size_in_kb',
|
||||
self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
|
||||
),
|
||||
'downloadChunkSizeInKb' => $dGlobal->getValInt(
|
||||
'dupcloud_download_chunk_size_in_kb',
|
||||
self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if config data has changed
|
||||
*
|
||||
* @param array<string,mixed> $newData New data to compare
|
||||
*
|
||||
* @return bool True if changed, false otherwise
|
||||
*/
|
||||
private function hasConfigChanged(array $newData): bool
|
||||
{
|
||||
foreach ($newData as $key => $value) {
|
||||
if (!isset($this->config[$key]) || $this->config[$key] !== $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Handler for DupCloud storage tokens via license integration
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Models;
|
||||
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Utils\Logging\DupLog;
|
||||
|
||||
/**
|
||||
* Handles storage tokens for quick connect from duplicator pro license
|
||||
*/
|
||||
class QuickConnect
|
||||
{
|
||||
/** @var array<string,mixed> */
|
||||
private static $tokens = [];
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
add_action('duplicator_license_check_remote_data_success', [self::class, 'handleLicenseDataSuccess'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update storage tokens
|
||||
*
|
||||
* @param LicenseData $license The license data instance
|
||||
* @param object $remoteData The remote data from license check
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function handleLicenseDataSuccess(LicenseData $license, $remoteData): void
|
||||
{
|
||||
if (
|
||||
!isset($remoteData->storage_tokens) ||
|
||||
!is_array($remoteData->storage_tokens)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($remoteData->storage_tokens as $tokenObj) {
|
||||
self::$tokens[] = [
|
||||
'license_key' => $tokenObj->license_key,
|
||||
'token' => $tokenObj->token,
|
||||
'product_name' => $tokenObj->product_name,
|
||||
'price_name' => $tokenObj->price_name,
|
||||
'expiration' => $tokenObj->expiration,
|
||||
'is_lifetime' => $tokenObj->is_lifetime,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage tokens by requesting fresh license data
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function getStorageTokens(): array
|
||||
{
|
||||
$licenseData = LicenseData::getInstance();
|
||||
|
||||
// Tokens are set with funciton handleLicenseDataSuccess inside getLicenseData using hooks
|
||||
add_filter('duplicator_license_request_params', [self::class, 'addStorageTokensRequest']);
|
||||
$licenseData->getLicenseData(true);
|
||||
remove_filter('duplicator_license_request_params', [self::class, 'addStorageTokensRequest']);
|
||||
|
||||
return self::$tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add storage tokens request parameter to license request
|
||||
*
|
||||
* @param array<string,mixed> $params Request parameters
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function addStorageTokensRequest($params)
|
||||
{
|
||||
$params['request_storage_tokens'] = 'true';
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license-based connection is available
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isLicenseConnectionAvailable(): bool
|
||||
{
|
||||
return class_exists(LicenseData:: class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Auloader calsses
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Utils;
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\DupCloudAddon;
|
||||
use Duplicator\Utils\AbstractAutoloader;
|
||||
|
||||
/**
|
||||
* Autoloader calss, dont user Duplicator library here
|
||||
*/
|
||||
final class Autoloader extends AbstractAutoloader
|
||||
{
|
||||
const VENDOR_PATH = DupCloudAddon::ADDON_PATH . '/vendor-prefixed/';
|
||||
|
||||
/**
|
||||
* Register autoloader function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void
|
||||
{
|
||||
spl_autoload_register([self::class, 'load']);
|
||||
|
||||
self::loadFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load class
|
||||
*
|
||||
* @param string $className class name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function load($className): void
|
||||
{
|
||||
foreach (self::getNamespacesVendorMapping() as $namespace => $mappedPath) {
|
||||
if (strpos($className, (string) $namespace) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filepath = self::getFilenameFromClass($className, $namespace, $mappedPath);
|
||||
if (file_exists($filepath)) {
|
||||
include $filepath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load necessary files
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function loadFiles(): void
|
||||
{
|
||||
$files = [
|
||||
'/guzzlehttp/promises/src/functions_include.php',
|
||||
'/guzzlehttp/psr7/src/functions_include.php',
|
||||
'/guzzlehttp/guzzle/src/functions_include.php',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
require_once self::VENDOR_PATH . $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return namespace mapping
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function getNamespacesVendorMapping(): array
|
||||
{
|
||||
return [
|
||||
self::ROOT_VENDOR . 'Psr\\Http\\Message' => self::VENDOR_PATH . '/psr/http-message/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Promise' => self::VENDOR_PATH . '/guzzlehttp/promises/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Psr7' => self::VENDOR_PATH . '/guzzlehttp/psr7/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp' => self::VENDOR_PATH . '/guzzlehttp/guzzle/src',
|
||||
];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Utils;
|
||||
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Duplicator\Utils\ExpireOptions;
|
||||
|
||||
/**
|
||||
* DupCloud rate limit handler
|
||||
*/
|
||||
final class DupCloudRateLimitHandler
|
||||
{
|
||||
const EXPIRE_DUPCLOUD_RATE_LIMIT_PREFIX = 'dup_cloud_rate_limit_route_';
|
||||
|
||||
const EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY = 'dup_cloud_rate_limit_error';
|
||||
|
||||
/**
|
||||
* Init
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
add_action('duplicator_dup_cloud_rate_limit_error', [self::class, 'handleRateLimitError'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any rate limit error is set
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasRateLimitError(): bool
|
||||
{
|
||||
return ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get retry after in seconds
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function retryAfter(): int
|
||||
{
|
||||
if (!ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ExpireOptions::getExpireTime(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY) - time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rate limit error
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param int $retryAfter Retry after
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function handleRateLimitError(string $url, int $retryAfter): void
|
||||
{
|
||||
if (!ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false)) {
|
||||
ExpireOptions::set(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, true, $retryAfter);
|
||||
}
|
||||
|
||||
ExpireOptions::set(self::getKey($url), true, $retryAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if route is rate limited
|
||||
*
|
||||
* @param string $url The URL to check
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isBlocked(string $url): bool
|
||||
{
|
||||
return ExpireOptions::get(self::getKey($url), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expire key from route
|
||||
*
|
||||
* @param string $url URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function getKey(string $url): string
|
||||
{
|
||||
$url = SnapIO::untrailingslashit($url);
|
||||
$id = sprintf('%u', crc32($url));
|
||||
return self::EXPIRE_DUPCLOUD_RATE_LIMIT_PREFIX . $id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,930 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Utils;
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Exceptions\PresignedUrlExpiredException;
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use Duplicator\Utils\Logging\DupLog;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Package\Create\PackInstaller;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Storage adapter to connect with Duplicator Cloud
|
||||
*/
|
||||
class DupCloudStorageAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_CHUNK_SIZE = 6 * MB_IN_BYTES;
|
||||
|
||||
private string $accessToken;
|
||||
private \Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient $client;
|
||||
/** @var resource */
|
||||
private $sourceFileHandle;
|
||||
/** @var string */
|
||||
private $lastSourceFilePath;
|
||||
/** @var resource */
|
||||
private $destFileHandle;
|
||||
/** @var string */
|
||||
private $lastDestFilePath = '';
|
||||
/** @var ?RemoteStorageInfo */
|
||||
private $remoteInfo;
|
||||
/** @var string */
|
||||
protected string $backupType = DupCloudClient::BACKUP_TYPE_STANDARD;
|
||||
/** @var int */
|
||||
private int $maxBackups = 0;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $accessToken The access accessKey
|
||||
* @param string $backupType The backup type
|
||||
* @param int $maxBackups The max backups
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $accessToken = '', string $backupType = DupCloudClient::BACKUP_TYPE_STANDARD, int $maxBackups = 0)
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
$this->client = new DupCloudClient($this->accessToken);
|
||||
$this->backupType = $backupType;
|
||||
$this->maxBackups = $maxBackups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = ''): bool
|
||||
{
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy(): bool
|
||||
{
|
||||
// Cloud storage is not destroyable
|
||||
if ($this->delete('/', true) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid and ready to use.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function realIsValid(string &$errorMsg = ''): bool
|
||||
{
|
||||
$remoteInfo = $this->remoteStorageInfo();
|
||||
if (!$remoteInfo->isSuccess()) {
|
||||
$errorMsg = __('Remote storage connection failed', 'duplicator-pro');
|
||||
return false;
|
||||
} elseif (!$remoteInfo->isAuthorized()) {
|
||||
$errorMsg = __('Storage configuration is invalid', 'duplicator-pro');
|
||||
return false;
|
||||
} elseif ($remoteInfo->getFreeSpace() <= 0) {
|
||||
$errorMsg = __('Storage is full', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote Storage info
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return RemoteStorageInfo
|
||||
*/
|
||||
protected function remoteStorageInfo(string &$errorMsg = ''): RemoteStorageInfo
|
||||
{
|
||||
if ($this->remoteInfo === null) {
|
||||
$this->remoteInfo = $this->client->remoteStorageInfo($errorMsg);
|
||||
}
|
||||
|
||||
return $this->remoteInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get free space in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFreeSpace(): int
|
||||
{
|
||||
return $this->remoteStorageInfo()->getFreeSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total available space in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalSpace(): int
|
||||
{
|
||||
return $this->remoteStorageInfo()->getTotalSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email associated with the storage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(): string
|
||||
{
|
||||
return $this->remoteStorageInfo()->getUserEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user name associated with the storage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(): string
|
||||
{
|
||||
return $this->remoteStorageInfo()->getUserName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the request was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthorized(): bool
|
||||
{
|
||||
return $this->remoteStorageInfo()->isAuthorized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the storage is ready for upload
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->remoteStorageInfo()->isReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the request was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->remoteStorageInfo()->isSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get website uuid
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWebsiteUuid(): string
|
||||
{
|
||||
return $this->remoteStorageInfo()->getWebsiteUuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(string $path): bool
|
||||
{
|
||||
if (DupCloudClient::isRootDir($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file with content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
* @param string $content The content of file.
|
||||
*
|
||||
* @return false The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCreateFile(string $path, string $content): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete relative 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(string $path, bool $recursive = false): bool
|
||||
{
|
||||
try {
|
||||
if ($this->isIncremental()) {
|
||||
throw new Exception('This method is not supported in incremental backup mode');
|
||||
}
|
||||
|
||||
// Extract backup name from path (get filename only)
|
||||
if (DupCloudClient::isRootDir($path)) {
|
||||
$result = $this->client->deleteAllBackups();
|
||||
if ($result) {
|
||||
return true;
|
||||
} else {
|
||||
DupLog::trace('Failed to delete all backups from website storage.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (DupCloudClient::isAllowedFileName($path)) {
|
||||
$result = $this->client->deleteFile($path);
|
||||
if ($result) {
|
||||
return true;
|
||||
} else {
|
||||
DupLog::trace('Failed to delete backup from cloud storage: ' . $path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Can only delete root dir or backup. Invalid Path: $path");
|
||||
} catch (Exception $e) {
|
||||
DupLog::traceException($e, 'Error deleting backup from cloud storage: ' . $path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
*
|
||||
* @return bool The content of file or false on failure.
|
||||
*/
|
||||
public function getFileContent(string $path): bool
|
||||
{
|
||||
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.
|
||||
*/
|
||||
protected function realMove(string $oldPath, string $newPath): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info and cache it, is path not exists return path info with exists property set to false.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo|false The path info or false on error.
|
||||
*/
|
||||
protected function getRealPathInfo(string $path)
|
||||
{
|
||||
try {
|
||||
if ($this->isIncremental()) {
|
||||
throw new Exception('This method is not supported in incremental backup mode');
|
||||
}
|
||||
|
||||
// For root path, return directory info
|
||||
if (DupCloudClient::isRootDir($path)) {
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = '/';
|
||||
$info->exists = true;
|
||||
$info->isDir = true;
|
||||
$info->size = 0;
|
||||
$info->modified = time();
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
if (DupCloudClient::isAllowedFileName($path)) {
|
||||
return $this->client->getFileInfo($path);
|
||||
} else {
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = $path;
|
||||
|
||||
return $info;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DupLog::traceException($e, 'Error getting path info for: ' . $path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(string $path, bool $files = true, bool $folders = true): array
|
||||
{
|
||||
try {
|
||||
if ($this->isIncremental()) {
|
||||
throw new Exception('This method is not supported in incremental backup mode');
|
||||
}
|
||||
|
||||
if (!DupCloudClient::isRootDir($path)) {
|
||||
DupLog::trace('Duplicator Cloud storage only supports scanning root directory, path provided: ' . $path);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$files) {
|
||||
DupLog::trace('Duplicator Cloud storage only contains files not directories.');
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileList = [];
|
||||
$storageInfolist = $this->client->getFileList();
|
||||
foreach ($storageInfolist as $storageInfo) {
|
||||
$fileList[] = $storageInfo->path;
|
||||
}
|
||||
|
||||
return $fileList;
|
||||
} catch (Exception $e) {
|
||||
DupLog::traceException($e, 'Error scanning cloud storage directory');
|
||||
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(string $path, array $filters = []): bool
|
||||
{
|
||||
try {
|
||||
if ($this->isIncremental()) {
|
||||
throw new Exception('This method is not supported in incremental backup mode');
|
||||
}
|
||||
|
||||
if (!DupCloudClient::isRootDir($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$backupList = $this->scanDir('/');
|
||||
if (count($backupList) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$regexFilters = [];
|
||||
$normalFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^\/.*\/$/', $filter) === 1) {
|
||||
$regexFilters[] = $filter;
|
||||
} else {
|
||||
$normalFilters[] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($backupList as $backupName) {
|
||||
if (in_array($backupName, $normalFilters)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($regexFilters as $regexFilter) {
|
||||
if (preg_match($regexFilter, $backupName) === 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered[] = $backupName;
|
||||
}
|
||||
|
||||
return count($filtered) === 0;
|
||||
} catch (Exception $e) {
|
||||
DupLog::traceException($e, 'Error checking if cloud storage directory is empty');
|
||||
// If we can't determine, assume it's not empty to be safe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This data is intended to be per-file and may be reset between files.
|
||||
* @param array<string,mixed> $generalExtraData Extra data to pass to copy function that persists across files
|
||||
* during the entire transfer operation.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCopyToStorage(
|
||||
string $sourceFile,
|
||||
string $storageFile,
|
||||
int $offset = 0,
|
||||
int $length = -1,
|
||||
int $timeout = 0,
|
||||
array &$extraData = [],
|
||||
array &$generalExtraData = []
|
||||
) {
|
||||
DupLog::infoTrace("Copying to Storage file " . $sourceFile . " to " . $storageFile);
|
||||
$startTime = microtime(true);
|
||||
$filename = basename($sourceFile);
|
||||
$fileSize = filesize($sourceFile);
|
||||
|
||||
try {
|
||||
if ($this->isFile($storageFile)) {
|
||||
$remainingBytes = ($fileSize - $offset) > 0 ? ($fileSize - $offset) : 0;
|
||||
DupLog::info("File already exists in cloud storage.");
|
||||
DupLog::info("Going to return remaining bytes: {$remainingBytes}bytes to satisfy the copy request");
|
||||
return $remainingBytes;
|
||||
}
|
||||
|
||||
if (!DupCloudClient::isAllowedFileName($storageFile)) {
|
||||
throw new Exception('Invalid backup name: ' . $storageFile);
|
||||
}
|
||||
|
||||
if (!is_file($sourceFile)) {
|
||||
throw new Exception("File not found at path: {$sourceFile}");
|
||||
}
|
||||
|
||||
// Only 2 types of files are allowed to be uploaded
|
||||
// 1. All the files that pass direct upload file type check will be uploaded directly
|
||||
// 2. All other files will have to be backup archives
|
||||
if (($type = self::getDirectUploadFileType($filename)) !== null) {
|
||||
if ($this->client->directUpload($sourceFile, $type, $storageFile) === false) {
|
||||
throw new Exception('Failed to upload installer file to cloud direcly');
|
||||
}
|
||||
|
||||
return $fileSize;
|
||||
} elseif (preg_match(DUPLICATOR_ARCHIVE_REGEX_PATTERN, $filename) !== 1) {
|
||||
throw new Exception('Invalid backup name: ' . $storageFile);
|
||||
}
|
||||
|
||||
if ($offset === 0) {
|
||||
if (!isset($generalExtraData['backup_details'])) {
|
||||
throw new Exception('Backup details not found in extra data');
|
||||
}
|
||||
|
||||
$backupDetails = $generalExtraData['backup_details'];
|
||||
if ($backupDetails['file_info']['backup_filename'] !== $storageFile) {
|
||||
throw new Exception('Backup name in backup details does not match the storage file name');
|
||||
}
|
||||
|
||||
if (($timeout === 0 && $length < 0) || ($fileSize <= $length)) {
|
||||
if ($this->client->upload($sourceFile, $backupDetails, $this->maxBackups) === false) {
|
||||
throw new Exception('Failed to upload file to cloud storage in single request.');
|
||||
}
|
||||
|
||||
return $fileSize;
|
||||
}
|
||||
|
||||
if (!isset($extraData['UploadUuid']) || !isset($extraData['UploadUrls'])) {
|
||||
$result = $this->client->startMultipart($backupDetails, $this->backupType);
|
||||
|
||||
$extraData['UploadUuid'] = $result['uuid'];
|
||||
$extraData['UploadUrls'] = $result['urls'];
|
||||
}
|
||||
} elseif (!isset($extraData['UploadUuid']) || $extraData['UploadUuid'] === false) {
|
||||
//the upload ID must exist if it's not the first chunk
|
||||
throw new Exception('Upload UUID has to be set to continue multipart upload');
|
||||
}
|
||||
|
||||
$partNumber = isset($extraData['Parts']) ? count($extraData['Parts']) + 1 : 1;
|
||||
if (($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false) {
|
||||
throw new Exception('Couldn\'t open source file for reading');
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length > 0 ? $length : self::DEFAULT_CHUNK_SIZE;
|
||||
do {
|
||||
if (
|
||||
fseek($sourceFileHandle, $offset) === -1 ||
|
||||
($content = fread($sourceFileHandle, $length)) === false
|
||||
) {
|
||||
throw new Exception('Couldn\'t read from source file');
|
||||
}
|
||||
|
||||
$this->uploadPart($partNumber, $content, $extraData);
|
||||
|
||||
if ($timeout === 0) {
|
||||
$bytesWritten = $length;
|
||||
break;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += $length;
|
||||
$partNumber++;
|
||||
} while (self::getElapsedTime($startTime) < $timeout && !feof($sourceFileHandle));
|
||||
|
||||
//finished upload
|
||||
if (feof($sourceFileHandle)) {
|
||||
if (!$this->client->completeMultipart($extraData['UploadUuid'], $extraData['Parts'], $this->maxBackups)) {
|
||||
throw new Exception('Failed to complete multipart upload');
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTrace('DupCloudStorageAdapter::realCopyToStorage: ' . $e->getMessage());
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::realCopyToStorage: ');
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a part
|
||||
*
|
||||
* @param int $partNumber The part number
|
||||
* @param string $content The content
|
||||
* @param array<string, mixed> $extraData Extra data
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If the upload fails for other reasons
|
||||
*/
|
||||
protected function uploadPart(int $partNumber, string $content, array &$extraData): void
|
||||
{
|
||||
if (!isset($extraData['UploadUrls'][$partNumber])) {
|
||||
$extraData['UploadUrls'] = $this->client->getPartUrls($extraData['UploadUuid'], $partNumber);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->client->uploadPart($extraData['UploadUrls'][$partNumber], $content);
|
||||
} catch (PresignedUrlExpiredException $e) {
|
||||
$extraData['UploadUrls'] = $this->client->getPartUrls($extraData['UploadUuid'], $partNumber);
|
||||
$this->client->uploadPart($extraData['UploadUrls'][$partNumber], $content);
|
||||
}
|
||||
|
||||
$extraData['Parts'][] = [
|
||||
'ETag' => md5($content),
|
||||
'PartNumber' => $partNumber,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if direct upload file name
|
||||
*
|
||||
* @param string $filename The file name
|
||||
*
|
||||
* @return ?string The file type or null if not a direct upload file
|
||||
*/
|
||||
public static function getDirectUploadFileType(string $filename): ?string
|
||||
{
|
||||
$localInstallerRegex = '/(^.+_[a-z0-9]{7,}_[0-9]{14})_(.*)' . preg_quote(PackInstaller::INSTALLER_SERVER_EXTENSION, '/') . '$/';
|
||||
if (preg_match($localInstallerRegex, $filename) === 1) {
|
||||
return 'installer';
|
||||
}
|
||||
|
||||
if (strpos($filename, DUPLICATOR_LOCAL_OVERWRITE_PARAMS) === 0) {
|
||||
return 'installer-params';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* This data is intended to be per-file and may be reset between files.
|
||||
* @param array<string,mixed> $generalExtraData Extra data to pass to copy function that persists across files
|
||||
* during the entire transfer operation.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
public function copyFromStorage(
|
||||
string $storageFile,
|
||||
string $destFile,
|
||||
int $offset = 0,
|
||||
int $length = -1,
|
||||
int $timeout = 0,
|
||||
array &$extraData = [],
|
||||
array &$generalExtraData = []
|
||||
) {
|
||||
$startTime = microtime(true);
|
||||
try {
|
||||
if (!DupCloudClient::isAllowedFileName($storageFile)) {
|
||||
throw new Exception('Invalid backup name: ' . $storageFile);
|
||||
}
|
||||
|
||||
$filename = basename($storageFile);
|
||||
if ($offset > 0 && !file_exists($destFile)) {
|
||||
throw new Exception('Destination file doesn\'t exist');
|
||||
}
|
||||
|
||||
if (wp_mkdir_p(dirname($destFile)) == false) {
|
||||
throw new Exception('Can\'t create parent folder');
|
||||
}
|
||||
|
||||
if (is_file($destFile) && $offset === 0 && !unlink($destFile)) {
|
||||
throw new Exception('Can\'t delete destination file');
|
||||
}
|
||||
|
||||
if (!$this->isFile($filename)) {
|
||||
throw new Exception('Backup file doesn\'t exist');
|
||||
}
|
||||
|
||||
if (!isset($extraData['download_url'])) {
|
||||
$extraData = $this->client->getDownloadData($filename);
|
||||
if (empty($extraData['download_url'])) {
|
||||
throw new Exception('Download URL not found');
|
||||
}
|
||||
}
|
||||
|
||||
if ($timeout === 0 && $offset === 0 && $length < 0) {
|
||||
if (($content = $this->client->downloadChunk($extraData['download_url'], 0, -1)) === false) {
|
||||
DupLog::infoTrace('Error downloading chunk: ' . print_r(
|
||||
[
|
||||
'url' => $extraData['download_url'],
|
||||
'offset' => $offset,
|
||||
'length' => $extraData['size'],
|
||||
],
|
||||
true
|
||||
));
|
||||
throw new Exception('Error downloading whole file');
|
||||
}
|
||||
|
||||
return file_put_contents($destFile, $content);
|
||||
}
|
||||
|
||||
if (($handle = $this->getDestFileHandle($destFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length > 0 ? $length : self::DEFAULT_CHUNK_SIZE;
|
||||
$downloadLenght = min($length, $extraData['size'] - $offset);
|
||||
do {
|
||||
if (($content = $this->client->downloadChunk($extraData['download_url'], $offset, $downloadLenght)) === false) {
|
||||
DupLog::infoTrace('Error downloading chunk: ' . print_r(
|
||||
[
|
||||
'url' => $extraData['download_url'],
|
||||
'offset' => $offset,
|
||||
'length' => $downloadLenght,
|
||||
],
|
||||
true
|
||||
));
|
||||
throw new Exception('Error downloading chunk');
|
||||
}
|
||||
|
||||
if (
|
||||
@ftruncate($handle, $offset) === false ||
|
||||
@fseek($handle, $offset) === -1 ||
|
||||
@fwrite($handle, $content) === false ||
|
||||
@fflush($handle) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += $length;
|
||||
} while (self::getElapsedTime($startTime) < $timeout && $offset < $extraData['size']);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::copyFromStorage ');
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elapsed time in microseconds
|
||||
*
|
||||
* @param float $startTime The start time
|
||||
*
|
||||
* @return float The elapsed time in microseconds
|
||||
*/
|
||||
private static function getElapsedTime(float $startTime): float
|
||||
{
|
||||
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage usage stats
|
||||
*
|
||||
* @return array{name:string,email:string,email_verified_at:string,created_at:string}
|
||||
*/
|
||||
public function getUserInfo(): array
|
||||
{
|
||||
return $this->client->getUserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke authorization
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function revokeAuthorization(): bool
|
||||
{
|
||||
return $this->client->revoke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an upload as failed on the remote server
|
||||
*
|
||||
* @param string $backupName The backup name to cancel
|
||||
*
|
||||
* @return bool True if cancelled, false otherwise
|
||||
*/
|
||||
public function failUploadByName(string $backupName): bool
|
||||
{
|
||||
try {
|
||||
if (strlen($backupName) === 0) {
|
||||
throw new Exception('Backup name is empty');
|
||||
}
|
||||
|
||||
return $this->client->failUploadByName($backupName);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::failUpload');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an upload as canceled on the remote server
|
||||
*
|
||||
* @param string $backupName The backup name to cancel
|
||||
*
|
||||
* @return bool True if cancelled, false otherwise
|
||||
*/
|
||||
public function cancelUploadByName(string $backupName): bool
|
||||
{
|
||||
try {
|
||||
if (strlen($backupName) === 0) {
|
||||
throw new Exception('Backup name is empty');
|
||||
}
|
||||
|
||||
return $this->client->cancelUploadByName($backupName);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::cancelUpload');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an upload as failed on the remote server
|
||||
*
|
||||
* @param string $uploadUuid The upload UUID to cancel
|
||||
*
|
||||
* @return bool True if cancelled, false otherwise
|
||||
*/
|
||||
public function failUpload(string $uploadUuid): bool
|
||||
{
|
||||
try {
|
||||
if (strlen($uploadUuid) === 0) {
|
||||
throw new Exception('Upload UUID is empty');
|
||||
}
|
||||
|
||||
return $this->client->failUpload($uploadUuid);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::failUpload');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an upload on the remote server
|
||||
*
|
||||
* @param string $uploadUuid The upload UUID to cancel
|
||||
*
|
||||
* @return bool True if cancelled, false otherwise
|
||||
*/
|
||||
public function cancelUpload(string $uploadUuid): bool
|
||||
{
|
||||
try {
|
||||
if (strlen($uploadUuid) === 0) {
|
||||
throw new Exception('Upload UUID is empty');
|
||||
}
|
||||
|
||||
return $this->client->cancelUpload($uploadUuid);
|
||||
} catch (Exception $e) {
|
||||
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::cancelUpload');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are in incremental mode
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isIncremental(): bool
|
||||
{
|
||||
return $this->backupType === DupCloudClient::BACKUP_TYPE_INCREMENTAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $sourceFilePath The source file path
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
private function getSourceFileHandle(string $sourceFilePath)
|
||||
{
|
||||
if ($this->lastSourceFilePath === $sourceFilePath) {
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (($this->sourceFileHandle = SnapIO::fopen($sourceFilePath, 'r')) === false) {
|
||||
throw new Exception('Can\'t open ' . $sourceFilePath . ' file');
|
||||
}
|
||||
|
||||
$this->lastSourceFilePath = $sourceFilePath;
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dest file handle
|
||||
*
|
||||
* @param string $destFilePath The dest file path
|
||||
*
|
||||
* @return resource|false The dest file handle or false on failure
|
||||
*/
|
||||
private function getDestFileHandle(string $destFilePath)
|
||||
{
|
||||
if ($this->lastDestFilePath === $destFilePath && is_resource($this->destFileHandle)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DupCloudAddon\Utils;
|
||||
|
||||
/**
|
||||
* Class to store and provide access to remote storage information
|
||||
*/
|
||||
class RemoteStorageInfo
|
||||
{
|
||||
/** @var bool Whether the request was successful */
|
||||
private bool $success = false;
|
||||
|
||||
/** @var bool Whether the storage is authorized */
|
||||
private bool $authorized = false;
|
||||
|
||||
/** @var bool Whether the storage is ready for upload */
|
||||
private bool $ready = false;
|
||||
|
||||
/** @var int Total available space in bytes */
|
||||
private int $totalSpace = 0;
|
||||
|
||||
/** @var int Free space in bytes */
|
||||
private int $freeSpace = 0;
|
||||
|
||||
/** @var string User name associated with the storage */
|
||||
private string $userName = '';
|
||||
|
||||
/** @var string User email associated with the storage */
|
||||
private string $userEmail = '';
|
||||
|
||||
/** @var string Website UUID */
|
||||
private string $websiteUuid = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param bool $success Whether the request was successful
|
||||
* @param bool $authorized Whether the storage is authorized
|
||||
* @param bool $ready Whether the storage is ready for upload
|
||||
* @param int $totalSpace Total available space in bytes
|
||||
* @param int $freeSpace Free space in bytes
|
||||
* @param string $userName User name associated with the storage
|
||||
* @param string $userEmail User email associated with the storage
|
||||
* @param string $websiteUuid Website UUID
|
||||
*/
|
||||
public function __construct(
|
||||
bool $success = false,
|
||||
bool $authorized = false,
|
||||
bool $ready = false,
|
||||
int $totalSpace = 0,
|
||||
int $freeSpace = 0,
|
||||
string $userName = '',
|
||||
string $userEmail = '',
|
||||
string $websiteUuid = ''
|
||||
) {
|
||||
$this->success = $success;
|
||||
$this->authorized = $authorized;
|
||||
$this->ready = $ready;
|
||||
$this->totalSpace = $totalSpace;
|
||||
$this->freeSpace = $freeSpace;
|
||||
$this->userName = $userName;
|
||||
$this->userEmail = $userEmail;
|
||||
$this->websiteUuid = $websiteUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the request was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the storage is authorized
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthorized(): bool
|
||||
{
|
||||
return $this->authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the storage is ready for upload
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total available space in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalSpace(): int
|
||||
{
|
||||
return $this->totalSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get free space in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFreeSpace(): int
|
||||
{
|
||||
return $this->freeSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user name associated with the storage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserName(): string
|
||||
{
|
||||
return $this->userName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user email associated with the storage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(): string
|
||||
{
|
||||
return $this->userEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get website UUID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWebsiteUuid(): string
|
||||
{
|
||||
return $this->websiteUuid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator Cloud Storage Provider Configs
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
*/
|
||||
|
||||
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
|
||||
$maxPackages = $tplMng->getDataValueIntRequired("maxPackages");
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
|
||||
<td class="dupcloud-authorize">
|
||||
<?php
|
||||
$tplMng->render(
|
||||
'dupcloudaddon/connect/storage_auth',
|
||||
['token_connection' => true]
|
||||
);
|
||||
?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="dupcloud_max_files"><?php esc_html_e("Max Backups", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<div class="horizontal-input-row">
|
||||
<input
|
||||
id="dupcloud_max_files"
|
||||
class="dupcloud_max_files margin-0"
|
||||
name="dupcloud_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#dupcloud_max_files_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-min="0"
|
||||
>
|
||||
<label for="dupcloud_max_files">
|
||||
<?php esc_html_e("Number of Backups to keep.", 'duplicator-pro'); ?><br/>
|
||||
</label>
|
||||
</div>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/max_backups_description'); ?>
|
||||
<div id="dupcloud_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
$tplMng->render('admin_pages/storages/parts/provider_foot');
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Template for Duplicator Cloud Connect Step 1
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
use Duplicator\Core\CapMng;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
*/
|
||||
|
||||
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
|
||||
$autoActivate = $tplMng->getDataValueBool('auto_activate_storage', false);
|
||||
|
||||
?>
|
||||
<div class="dup-settings-wrapper" >
|
||||
<h3 class="title">
|
||||
<?php esc_html_e('Duplicator Cloud', 'duplicator-pro') ?>
|
||||
</h3>
|
||||
<hr size="1">
|
||||
<?php if ($storage->isAuthorized()) { ?>
|
||||
<label class="lbl-larger">
|
||||
<?php esc_html_e("Cloud Info", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<?php } else { ?>
|
||||
<p>
|
||||
<?php esc_html_e('Connect to Duplicator Cloud Storage.', 'duplicator-pro'); ?>
|
||||
</p>
|
||||
<label class="lbl-larger">
|
||||
<?php esc_html_e("Authorization", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<?php } ?>
|
||||
<div class="margin-bottom-1">
|
||||
<form
|
||||
id="dup-storage-form"
|
||||
method="post"
|
||||
data-parsley-ui-enabled="true"
|
||||
target="_self"
|
||||
>
|
||||
<input type="hidden" name="storage_id" id="storage_id" value="<?php echo (int) $storage->getId(); ?>">
|
||||
<input type="hidden" id="name" name="name" value="<?php echo esc_attr($storage->getName()); ?>">
|
||||
<?php $tplMng->render('dupcloudaddon/connect/storage_auth'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$tplMng->render('admin_pages/storages/storage_scripts');
|
||||
|
||||
|
||||
if ($autoActivate && CapMng::can(CapMng::CAP_STORAGE, false)) {
|
||||
?>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
var $button = $('#dupli-dupcloud-license-connect-btn');
|
||||
if ($button.length) {
|
||||
DupliJs.Storage.DupCloud.LicenseConnect($button, true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator Cloud global storage options
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
?>
|
||||
<div class="dup-accordion-wrapper display-separators close" >
|
||||
<div class="accordion-header" >
|
||||
<h3 class="title"><?php echo esc_html(DupCloudStorage::getStypeName()); ?></h3>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<label class="lbl-larger" >
|
||||
<?php esc_html_e("Upload Chunk Size", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<div class="margin-bottom-1" >
|
||||
<input
|
||||
class="text-right inline-display width-small margin-bottom-0"
|
||||
name="dupcloud_upload_chunk_size_in_kb"
|
||||
id="dupcloud_upload_chunk_size_in_kb"
|
||||
type="number"
|
||||
min="<?php echo (int) DupCloudStorage::UPLOAD_CHUNK_MIN_SIZE_IN_KB; ?>"
|
||||
max="<?php echo (int) DupCloudStorage::UPLOAD_CHUNK_MAX_SIZE_IN_KB; ?>"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#dupcloud_upload_chunk_size_in_kb_error_container"
|
||||
value="<?php echo (int) $tplData['uploadChunkSizeInKb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="dupcloud_upload_chunk_size_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be uploaded to Duplicator Cloud per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
|
||||
<?php echo esc_html(sprintf(__('Min size %skb.', 'duplicator-pro'), DupCloudStorage::UPLOAD_CHUNK_MIN_SIZE_IN_KB)); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="lbl-larger" >
|
||||
<?php esc_html_e("Download Chunk Size", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<div class="margin-bottom-1" >
|
||||
<input
|
||||
class="text-right inline-display width-small margin-bottom-0"
|
||||
name="dupcloud_download_chunk_size_in_kb"
|
||||
id="dupcloud_download_chunk_size_in_kb"
|
||||
type="number"
|
||||
min="<?php echo (int) DupCloudStorage::DOWNLOAD_CHUNK_MIN_SIZE_IN_KB; ?>"
|
||||
max="<?php echo (int) DupCloudStorage::DOWNLOAD_CHUNK_MAX_SIZE_IN_KB; ?>"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#dupcloud_download_chunk_size_in_kb_error_container"
|
||||
value="<?php echo (int) $tplData['downloadChunkSizeInKb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="dupcloud_download_chunk_size_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be downloaded from Duplicator Cloud per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
|
||||
<?php echo esc_html(sprintf(__('Min size %skb.', 'duplicator-pro'), DupCloudStorage::DOWNLOAD_CHUNK_MIN_SIZE_IN_KB)); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Template for Duplicator Cloud Connect Step 1
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
*/
|
||||
|
||||
$tokens = $tplMng->getDataValueArrayRequired('tokens');
|
||||
|
||||
switch (count($tokens)) {
|
||||
case 0:
|
||||
?>
|
||||
<div class="dup-box">
|
||||
<div class="dup-box-title">
|
||||
<i class="fa fa-cloud-off"></i>
|
||||
<?php esc_html_e('No Storage Available', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
<div class="dup-box-panel" >
|
||||
<p>
|
||||
<?php esc_html_e(
|
||||
'No Duplicator Cloud storage is available for your license. Purchase cloud storage to use this feature.',
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</p>
|
||||
<a
|
||||
href="<?php echo esc_url(DupCloudClient::getManageLicenseStorageUrl()); ?>"
|
||||
class="button button-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa fa-shopping-cart"></i> <?php esc_html_e('Purchase Storage', 'duplicator-pro'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
break;
|
||||
case 1:
|
||||
// 1 token found, auto connect - no HTML needed, handled by JS
|
||||
break;
|
||||
default:
|
||||
?>
|
||||
<div class="dup-box">
|
||||
<div class="dup-box-title">
|
||||
<?php esc_html_e('Select Storage', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
<div class="dup-box-panel" >
|
||||
<p>
|
||||
<?php esc_html_e(
|
||||
'Multiple storage options are available for your license. Please select one:',
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</p>
|
||||
<div id="storage-selection-list">
|
||||
<?php foreach ($tokens as $index => $token) : ?>
|
||||
<?php
|
||||
$maskedLicense = substr($token['license_key'], 0, 4) . '***';
|
||||
$expiration = $token['is_lifetime'] ?
|
||||
__('Lifetime', 'duplicator-pro') :
|
||||
date_i18n(get_option('date_format'), strtotime($token['expiration']));
|
||||
?>
|
||||
<div class="dup-storage-item margin-bottom-1" >
|
||||
<strong><?php echo esc_html($token['price_name']); ?></strong> |
|
||||
<?php esc_html_e('License:', 'duplicator-pro'); ?> <?php echo esc_html($maskedLicense); ?> |
|
||||
<?php esc_html_e('Expires:', 'duplicator-pro'); ?> <?php echo esc_html($expiration); ?>
|
||||
<button type="button" class="button button-primary small margin-bottom-0" data-token-select="<?php echo esc_attr($index); ?>">
|
||||
<?php esc_html_e('Use This Storage', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Template for Duplicator Cloud Connect Step 1
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Ajax\ServicesDupCloud;
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
|
||||
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
|
||||
use Duplicator\Libs\Snap\SnapString;
|
||||
use Duplicator\Views\UI\UiDialog;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
*/
|
||||
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
|
||||
$errorMsg = '';
|
||||
$showLicenseConnection = QuickConnect::isLicenseConnectionAvailable();
|
||||
$showTokenConnection = $tplMng->getDataValueBool('token_connection', false);
|
||||
$bothConnection = $showLicenseConnection && $showTokenConnection;
|
||||
|
||||
if (!$storage->isAuthorized()) : ?>
|
||||
<div class='dupcloud-authorization-state' id="dupcloud-state-unauthorized">
|
||||
<?php if ($showLicenseConnection) { ?>
|
||||
<button
|
||||
id="dupli-dupcloud-license-connect-btn"
|
||||
type="button"
|
||||
class="button secondary hollow margin-bottom-0"
|
||||
>
|
||||
<i class="fa fa-key"></i> <?php esc_html_e('Connect via Duplicator Pro License', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
<?php
|
||||
}
|
||||
if ($bothConnection) { ?>
|
||||
<strong>or</strong>
|
||||
<?php
|
||||
}
|
||||
if ($showTokenConnection) { ?>
|
||||
<button
|
||||
id="dupli-dupcloud-connect-btn"
|
||||
type="button"
|
||||
class="button secondary hollow margin-bottom-0"
|
||||
onclick="DupliJs.Storage.DupCloud.ShowTokenInput();">
|
||||
<i class="fa fa-plug"></i> <?php esc_html_e('Connect Duplicator Cloud Token', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
<?php } ?>
|
||||
|
||||
<div id="dupli-dupcloud-token-area" style="display:none;">
|
||||
<div class="storage-auth-step">
|
||||
<p>
|
||||
<b><?php esc_html_e('Step 1:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e('Get your authentication token from Duplicator.com', 'duplicator-pro'); ?>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="button secondary hollow margin-bottom-0"
|
||||
onclick="window.open('<?php echo esc_js(DupCloudClient::getManageLicenseStorageUrl()); ?>', '_blank');">
|
||||
<i class="fa fa-external-link"></i> <?php esc_html_e('Get Connection Token', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="storage-auth-step">
|
||||
<p>
|
||||
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e('Paste your authentication token below:', 'duplicator-pro'); ?>
|
||||
</p>
|
||||
<input id="dupcloud-compound-token" name="dupcloud-compound-token" style="width: 500px" type="text">
|
||||
</div>
|
||||
|
||||
<div class="storage-auth-step">
|
||||
<button
|
||||
id="dupcloud-finalize-setup"
|
||||
type="button"
|
||||
class="button secondary margin-bottom-0">
|
||||
<i class="fa fa-check-square"></i> <?php esc_html_e('Finalize Setup', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class='dupcloud-authorization-state' id="dupcloud-state-authorized" style="margin-top:-10px">
|
||||
<?php if (strlen($storage->getUserName()) > 0) : ?>
|
||||
<h3>
|
||||
<?php esc_html_e('Duplicator Cloud Account', 'duplicator-pro'); ?><br />
|
||||
<i class="dupli-edit-info">
|
||||
<?php esc_html_e('Duplicator has been authorized to access this user\'s Duplicator Cloud account', 'duplicator-pro'); ?>
|
||||
</i>
|
||||
</h3>
|
||||
<?php if (!$storage->isValid($errorMsg, true)) : ?>
|
||||
<div class="alert-color margin-bottom-1">
|
||||
<p><b><?php esc_html_e('The storage is currently not in a valid state.', 'duplicator-pro'); ?></b></p>
|
||||
<p><b><?php esc_html_e('Error:', 'duplicator-pro'); ?></b> <?php echo esc_html($errorMsg); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div id="dupcloud-account-info">
|
||||
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($storage->getUserName()); ?><br />
|
||||
|
||||
<label><?php esc_html_e('Email', 'duplicator-pro'); ?>:</label> <?php echo esc_html($storage->getUserEmail()); ?><br />
|
||||
<label><?php esc_html_e('Space', 'duplicator-pro'); ?>:</label>
|
||||
<?php if ($storage->getFreeSpace() > 0) : ?>
|
||||
<?php printf(
|
||||
'%1$s of %2$s is is used',
|
||||
esc_html(SnapString::byteSize($storage->getUsedSpace())),
|
||||
esc_html(SnapString::byteSize($storage->getTotalSpace()))
|
||||
); ?>
|
||||
<?php else : ?>
|
||||
<b class="alert-color">
|
||||
<?php printf(
|
||||
'%1$s of %2$s is used',
|
||||
esc_html(SnapString::byteSize($storage->getUsedSpace())),
|
||||
esc_html(SnapString::byteSize($storage->getTotalSpace()))
|
||||
); ?>
|
||||
</b>
|
||||
<br />
|
||||
<br />
|
||||
<b class="alert-color"><?php esc_html_e('Warning! Storage is full and cannot be used.', 'duplicator-pro'); ?></b>
|
||||
<?php endif ?>
|
||||
</div><br />
|
||||
<?php else : ?>
|
||||
<div><?php esc_html_e('Error retrieving user information.', 'duplicator-pro'); ?></div>
|
||||
<?php endif ?>
|
||||
<a href="<?php echo esc_url($storage->getBackupsUrl()); ?>" target="_blank"
|
||||
id="dup-dupcloud-manage-website"
|
||||
class="button margin-right-1 button-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<?php esc_html_e('Manage Backups', 'duplicator-pro'); ?>
|
||||
</a>
|
||||
<button
|
||||
id="dup-dupcloud-cancel-authorization"
|
||||
type="button"
|
||||
class="button gray hollow">
|
||||
<?php esc_html_e('Cancel Authorization', 'duplicator-pro'); ?>
|
||||
</button><br />
|
||||
<i class="dupli-edit-info">
|
||||
<?php
|
||||
esc_html_e(
|
||||
'Disassociation of storage provider will require re-authorization.',
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</div>
|
||||
<?php endif;
|
||||
|
||||
|
||||
$alertConnStatus = new UiDialog();
|
||||
$alertConnStatus->title = __('Duplicator Cloud Authorization Error', 'duplicator-pro');
|
||||
$alertConnStatus->message = ''; // javascript inserted message
|
||||
$alertConnStatus->initAlert();
|
||||
?>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
DupliJs.Storage.DupCloud = DupliJs.Storage.DupCloud || {};
|
||||
|
||||
DupliJs.Storage.DupCloud.ShowTokenInput = function() {
|
||||
$('#dupli-dupcloud-connect-btn-area').hide();
|
||||
$('#dupli-dupcloud-token-area').show();
|
||||
}
|
||||
|
||||
DupliJs.Storage.DupCloud.LicenseConnect = function($button, $skipNoLicenses = false) {
|
||||
var originalText = $button.html();
|
||||
$button.html('<i class="fa fa-spinner fa-spin"></i> <?php esc_html_e("Getting tokens...", "duplicator-pro"); ?>').prop('disabled', true);
|
||||
|
||||
// AJAX call to get storage tokens
|
||||
DupliJs.Util.ajaxWrapper({
|
||||
action: '<?php echo esc_js(ServicesDupCloud::AJAX_ACTION_QUICK_CONNECT); ?>',
|
||||
nonce: '<?php echo esc_js(wp_create_nonce(ServicesDupCloud::AJAX_ACTION_QUICK_CONNECT)); ?>'
|
||||
},
|
||||
function(result, data, funcData, textStatus, jqXHR) {
|
||||
|
||||
if (result.success) {
|
||||
var tokens = funcData.tokens;
|
||||
var modalHtml = funcData.html;
|
||||
|
||||
if (tokens.length === 0) {
|
||||
$button.html(originalText).prop('disabled', false);
|
||||
if ($skipNoLicenses == false) {
|
||||
// No storage tokens available - show modal with purchase message
|
||||
DupliJs.Storage.DupCloudLicense.showModal(modalHtml);
|
||||
}
|
||||
} else if (tokens.length === 1) {
|
||||
// Single storage - auto connect (no modal)
|
||||
DupliJs.Storage.DupCloudLicense.autoConnect(tokens[0]);
|
||||
} else {
|
||||
// Multiple storages - show selection modal
|
||||
DupliJs.Storage.DupCloudLicense.showModal(modalHtml, tokens);
|
||||
}
|
||||
} else {
|
||||
$button.html(originalText).prop('disabled', false);
|
||||
|
||||
// Skip error messages in silence mode
|
||||
if (data.message && $skipNoLicenses == false) {
|
||||
DupliJs.addAdminMessage(data.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#dup-dupcloud-manage-website').click(function(e) {
|
||||
e.stopPropagation();
|
||||
window.open('<?php echo esc_js(DupCloudClient::manageWebsitesUrl()); ?>', '_blank');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#dup-dupcloud-cancel-authorization').click(function(e) {
|
||||
e.stopPropagation();
|
||||
DupliJs.Storage.RevokeAuth(<?php echo (int) $storage->getId(); ?>);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#dupcloud-finalize-setup').click(function(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
var compoundToken = $('#dupcloud-compound-token').val().trim();
|
||||
|
||||
if (compoundToken.length > 0) {
|
||||
// Validate token format (should contain a dot)
|
||||
if (compoundToken.indexOf('.') === -1) {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Invalid token format. Please ensure you copied the complete authentication token.', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
return false;
|
||||
}
|
||||
|
||||
DupliJs.Storage.PrepareForSubmit();
|
||||
|
||||
DupliJs.Storage.Authorize(
|
||||
<?php echo (int) $storage->getId(); ?>,
|
||||
<?php echo (int) $storage->getSType(); ?>, {
|
||||
'name': $('#name').val(),
|
||||
'notes': $('#notes').val(),
|
||||
'access_token': compoundToken // Send compound token as access_token
|
||||
}
|
||||
);
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Please paste your authentication token!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// License-based connection handler
|
||||
$('#dupli-dupcloud-license-connect-btn').click(function(event) {
|
||||
event.stopPropagation();
|
||||
var $button = $(this);
|
||||
DupliJs.Storage.DupCloud.LicenseConnect($button);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
// License connection namespace
|
||||
DupliJs.Storage.DupCloudLicense = DupliJs.Storage.DupCloudLicense || {};
|
||||
|
||||
DupliJs.Storage.DupCloudLicense.showModal = function(htmlContent, tokens = null) {
|
||||
var modal = new DuplicatorModalBox({
|
||||
htmlContent: htmlContent,
|
||||
closeInContent: true,
|
||||
closeColor: "#000",
|
||||
openCallback: function(content, modalObj) {
|
||||
// If tokens provided, setup click handlers for storage selection
|
||||
if (tokens && tokens.length > 1) {
|
||||
$(content).find('[data-token-select]').click(function() {
|
||||
var tokenIndex = $(this).data('token-select');
|
||||
if (tokens[tokenIndex]) {
|
||||
modalObj.close();
|
||||
DupliJs.Storage.DupCloudLicense.autoConnect(tokens[tokenIndex]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
DupliJs.Storage.DupCloudLicense.autoConnect = function(storageToken) {
|
||||
// Set the token in the input field
|
||||
$('#dupcloud-compound-token').val(storageToken.token);
|
||||
|
||||
// Automatically trigger finalize setup
|
||||
setTimeout(function() {
|
||||
$('#dupcloud-finalize-setup').click();
|
||||
}, 100);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Template for Duplicator Cloud Connect Step 1
|
||||
*
|
||||
* @package Duplicator\Addons\DupCloudAddon
|
||||
* @copyright (c) 2024, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
|
||||
use Duplicator\Core\CapMng;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
*/
|
||||
$storage = DupCloudStorage::getUniqueStorage();
|
||||
|
||||
if (
|
||||
!CapMng::can(CapMng::CAP_STORAGE, false) ||
|
||||
!$storage->isAuthorized()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<span>
|
||||
<a href="<?php echo esc_url($storage->getBackupsUrl()); ?>" target="_blank"
|
||||
id="dup-dupcloud-manage-website"
|
||||
class="button button-primary hollow tiny font-bold margin-bottom-0"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-solid fa-cloud"></i>
|
||||
<?php esc_html_e('Cloud Dashboard', 'duplicator-pro'); ?>
|
||||
</a>
|
||||
</span>
|
||||
Reference in New Issue
Block a user