first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
<?php
namespace Duplicator\Addons\DropboxAddon;
use Duplicator\Addons\DropboxAddon\Models\DropboxStorage;
use Duplicator\Addons\DropboxAddon\Utils\Autoloader;
use Duplicator\Core\Addons\AbstractAddonCore;
use Duplicator\Models\Storages\AbstractStorageEntity;
class DropboxAddon extends AbstractAddonCore
{
const ADDON_PATH = __DIR__;
/**
* @return void
*/
public function init(): void
{
Autoloader::register();
add_action('duplicator_register_storage_types', [$this, 'registerStorages']);
add_filter('duplicator_template_file', [self::class, 'getTemplateFile'], 10, 2);
add_filter('duplicator_usage_stats_storages_infos', [self::class, 'getStorageUsageStats'], 10);
}
/**
* 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, 'dropboxaddon/') === 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_dropbox_count'] = 0;
foreach ($storages as $storage) {
if ($storage->getSType() === DropboxStorage::getSType()) {
$storageNums['storages_dropbox_count']++;
}
}
return $storageNums;
}
/**
* Register storages
*
* @return void
*/
public function registerStorages(): void
{
DropboxStorage::registerType();
}
/**
*
* @return string
*/
public static function getAddonPath(): string
{
return self::ADDON_PATH;
}
/**
*
* @return string
*/
public static function getAddonFile(): string
{
return __FILE__;
}
}

View File

@@ -0,0 +1,609 @@
<?php
namespace Duplicator\Addons\DropboxAddon\Models;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Addons\DropboxAddon\Utils\DropboxClient;
use Duplicator\Models\Storages\AbstractStorageAdapter;
use Duplicator\Models\Storages\StoragePathInfo;
use Error;
use Exception;
use VendorDuplicator\Spatie\Dropbox\UploadSessionCursor;
class DropboxAdapter extends AbstractStorageAdapter
{
/** @var string */
protected $accessToken = '';
protected DropboxClient $client;
protected string $storageFolder;
/** @var bool */
protected $sslVerify = true;
/** @var string If empty use server cert else use custom cert path */
protected $sslCert = '';
/** @var bool */
protected $ipv4Only = false;
/**
* @param string $accessToken Dropbox access token.
* @param string $storageFolder Dropbox storage folder.
* @param bool $sslVerify If true, use SSL
* @param string $sslCert If empty use server cert
* @param bool $ipv4Only If true, use IPv4 only
*/
public function __construct(
$accessToken,
$storageFolder = '',
$sslVerify = true,
$sslCert = '',
$ipv4Only = false
) {
$this->accessToken = $accessToken;
$this->storageFolder = '/' . trim($storageFolder, '/') . '/';
$this->sslVerify = $sslVerify;
$this->sslCert = $sslCert;
$this->ipv4Only = $ipv4Only;
$this->client = new DropboxClient(
$accessToken,
null,
DropboxClient::MAX_CHUNK_SIZE,
0,
$sslVerify,
$sslCert,
$ipv4Only
);
}
/**
* Get the Dropbox client.
*
* @return DropboxClient
*/
public function getClient(): DropboxClient
{
return $this->client;
}
/**
* 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
{
if (! $this->exists('/')) {
try {
$this->createDir('/');
} catch (Exception $e) {
DupLog::trace($e->getMessage());
$errorMsg = $e->getMessage();
return false;
}
}
return true;
}
/**
* Destroy the storage on deletion.
*
* @return bool true on success or false on failure.
*/
public function destroy(): bool
{
$this->delete('/', true);
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
{
try {
$this->client->getMetadata($this->storageFolder);
} catch (Exception $e) {
DupLog::trace("Dropbox storage is invalid: " . $e->getMessage());
$errorMsg = $e->getMessage();
return false;
}
return true;
}
/**
* Create the directory specified by pathname, recursively if necessary.
*
* @param string $path The directory path.
*
* @return bool true on success or false on failure.
*/
protected function realCreateDir(string $path): bool
{
$path = $this->formatPath($path);
try {
$this->client->createFolder($path);
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
return true;
}
/**
* Create file with content.
*
* @param string $path The path to file.
* @param string $content The content of file.
*
* @return false|int The number of bytes that were written to the file, or false on failure.
*/
protected function realCreateFile(string $path, string $content)
{
$path = $this->formatPath($path);
try {
$response = $this->client->upload($path, $content, 'overwrite');
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
return $response['size'];
}
/**
* 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
{
$path = $this->formatPath($path);
if (! $recursive) {
try {
$response = $this->client->listFolder($path);
if (count($response['entries']) > 0) {
return false;
}
} catch (Exception $e) {
// Path is not a directory, so we can delete it.
}
}
try {
$this->client->delete($path);
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
return true;
}
/**
* Get file content.
*
* @param string $path The path to file.
*
* @return string|false The content of file or false on failure.
*/
public function getFileContent(string $path)
{
$content = '';
try {
$stream = $this->client->download($this->formatPath($path));
while ($chunk = fgets($stream)) {
$content .= $chunk;
}
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
return $content;
}
/**
* Move and/or rename a file or directory.
*
* @param string $oldPath Relative storage path
* @param string $newPath Relative storage path
*
* @return bool true on success or false on failure.
*/
protected function realMove(string $oldPath, string $newPath): bool
{
$oldPath = $this->formatPath($oldPath);
$newPath = $this->formatPath($newPath);
try {
$this->client->move($oldPath, $newPath);
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
return true;
}
/**
* 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 {
$response = $this->client->getMetadata($this->formatPath($path));
} catch (Exception $e) {
$response = [];
}
return $this->buildPathInfo($response);
}
/**
* 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
{
$path = rtrim($this->formatPath($path), '/') . '/';
$filterFunc = function ($entry) use ($files, $folders): bool {
if ($entry['.tag'] === 'file' && $files) {
return true;
}
if ($entry['.tag'] === 'folder' && $folders) {
return true;
}
return false;
};
try {
$response = $this->client->listFolder($path);
} catch (Exception $e) {
DupLog::trace('[DropboxAddon] ' . $e->getMessage());
return [];
}
// We filter out the entries as needed, then only keep the path.
// We do this early to keep the memory usage as low as possible.
$entries = array_map(fn($entry): string => substr($entry['path_display'], strlen($path)), array_filter($response['entries'], $filterFunc));
while ($response['has_more']) {
$response = $this->client->listFolderContinue($response['cursor']);
$entries = array_merge(
$entries,
array_map(
fn($entry): string => substr($entry['path_display'], strlen($path)),
array_filter($response['entries'], $filterFunc)
)
);
}
return $entries;
}
/**
* 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
{
$path = $this->formatPath($path);
try {
$response = $this->client->listFolder($path);
} catch (Exception $e) {
DupLog::trace($e->getMessage());
return false;
}
if (count($response['entries']) === 0) {
return true;
} elseif (empty($filters)) {
// we have no filters, and the folder is not empty, so it must contain something
return false;
}
$regexFilters = $normalFilters = [];
foreach ($filters as $filter) {
if ($filter[0] === '/' && substr($filter, -1) === '/') {
$regexFilters[] = $filter; // It's a regex filter as it starts and ends with a slash
} else {
$normalFilters[] = $filter;
}
}
$contents = $this->scanDir($path);
foreach ($contents as $item) {
if (in_array($item, $normalFilters)) {
continue;
}
foreach ($regexFilters as $regexFilter) {
if (preg_match($regexFilter, $item) === 1) {
continue 2;
}
}
return false;
}
return true;
}
/**
* Copy local file to storage, partial copy is supported.
* If destination file exists, it will be overwritten.
* If offset is less than the destination file size, the file will be truncated.
*
* @param string $sourceFile The source file full path
* @param string $storageFile Storage destination path
* @param int<0,max> $offset The offset where the data starts.
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
* 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 = []
) {
try {
$storageFile = $this->formatPath($storageFile);
$fileSize = filesize($sourceFile);
$chunkSize = $length > 0 ? $length : 4 * MB_IN_BYTES;
$fileKey = md5($sourceFile . $storageFile);
$completeKey = $fileKey . '_complete';
$result = false;
if (isset($extraData[$completeKey]) && $extraData[$completeKey] === true) {
// The file is already uploaded
return $fileSize;
}
// Check if we can read the source file
if (!$handle = @fopen($sourceFile, 'rb')) {
throw new Exception("Could not open source file: {$sourceFile}");
}
if ($fileSize <= $chunkSize || $length < 0) {
// We need to upload the whole file in one go
if (($file = $this->uploadCompleteFile($handle, $storageFile, $chunkSize)) == false) {
throw new Exception("Failed to upload file: " . $storageFile);
}
if (! isset($file['.tag']) || $file['.tag'] !== 'file') {
throw new Exception("Failed to upload file: " . json_encode($file));
}
$extraData[$completeKey] = true;
$result = $fileSize;
} else {
// At this point we know we need to upload the file in sequential chunks.
if (fseek($handle, $offset) !== 0) {
throw new Exception("Could not seek to offset {$offset} in source file: {$sourceFile}");
}
$cursor = null;
if (!empty($extraData[$fileKey])) {
$sessionId = $extraData[$fileKey];
$cursor = new UploadSessionCursor($sessionId, $offset);
}
$contents = @fread($handle, $chunkSize);
if ($cursor === null) {
// We need to start a new session as we don't have a cursor yet
$cursor = $this->client->uploadSessionStart($contents);
$extraData[$fileKey] = $cursor->session_id;
} elseif (strlen($contents) < $chunkSize) {
// As the content size is less than the chunk size, we need to finish the session
$this->client->uploadSessionFinish($contents, $cursor, $storageFile, 'overwrite');
$extraData[$completeKey] = true;
$cursor->offset += $chunkSize;
} else {
// A session is already started, we can append to it
$cursor = $this->client->uploadSessionAppend($contents, $cursor);
$extraData[$fileKey] = $cursor->session_id;
}
$result = $cursor->offset - $offset;
}
} catch (Exception | Error $e) {
$this->client->setTimeout(0);
DupLog::infoTraceException($e, "[DROPBOX] CopyToStorage error");
return false;
}
return $result;
}
/**
* Upload a whole file in one go
*
* @param resource $sourceHandle Resource handle for the file we are uploading
* @param string $storageFile Storage path for the uploaded file
* @param int $chunkSize Chunk size to use when uploading
*
* @return array<string, string>|false
*/
protected function uploadCompleteFile($sourceHandle, string $storageFile, int $chunkSize)
{
if (@fseek($sourceHandle, 0) !== 0) {
DupLog::info("[DropboxAddon] Could not seek to start of source file for {$storageFile}");
return false;
}
$cursor = $this->client->uploadSessionStart(@fread($sourceHandle, $chunkSize));
$file = null;
while (!feof($sourceHandle)) {
$contents = @fread($sourceHandle, $chunkSize);
if ($contents === false) {
return false;
}
if (strlen($contents) < $chunkSize) {
$file = $this->client->uploadSessionFinish($contents, $cursor, $storageFile, 'overwrite');
break;
}
$cursor = $this->client->uploadSessionAppend($contents, $cursor);
}
if ($file === null) {
$file = $this->client->uploadSessionFinish('', $cursor, $storageFile, 'overwrite');
}
return $file;
}
/**
* Normalize path, add storage root path if needed.
*
* @param string $path Relative storage path.
*
* @return string
*/
protected function formatPath($path): string
{
return $this->storageFolder . ltrim($path, '/');
}
/**
* Build StoragePathInfo object from Dropbox API response.
*
* @param array<string,mixed> $response Dropbox API response.
*
* @return StoragePathInfo
*/
protected function buildPathInfo($response)
{
$info = new StoragePathInfo();
$info->exists = isset($response['.tag']);
if (!$info->exists) {
return $info;
}
$info->path = $this->getRelativeStoragePath($response['path_display']);
$info->isDir = $response['.tag'] === 'folder';
$info->size = $response['size'] ?? 0;
$info->created = isset($response['client_modified']) ? strtotime($response['client_modified']) : time();
$info->modified = isset($response['server_modified']) ? strtotime($response['server_modified']) : time();
return $info;
}
/**
* Get relative storage path from Dropbox path display.
*
* @param string $path_display Dropbox path display.
* @param string $subPath Sub path to remove from the path display.
*
* @return string
*/
protected function getRelativeStoragePath($path_display, $subPath = ''): string
{
$rootPath = $this->storageFolder;
if (!empty($subPath)) {
$rootPath .= trim($subPath) . '/';
}
return substr($path_display, strlen($rootPath));
}
/**
* 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 = []
) {
if (! $this->exists($storageFile)) {
DupLog::trace("[DropboxAddon] Storage file {$storageFile} does not exist");
return false;
}
if ($offset > 0 && !@file_exists($destFile)) {
return false;
}
if (! isset($extraData['resuming']) && file_put_contents($destFile, '') === false) {
DupLog::trace("[DropboxAddon] Could not open destination file for writing. File: {$destFile}");
return false;
}
$extraData['resuming'] = true;
if (!isset($extraData['fileSize'])) {
$extraData['fileSize'] = $this->getPathInfo($storageFile)->size;
}
$this->client->setTimeout($timeout / SECONDS_IN_MICROSECONDS);
$bytesWritten = $offset;
$chunkSize = $length > 0 ? $length : 5 * MB_IN_BYTES;
while ($bytesWritten < $extraData['fileSize'] && ($length < 0 || $bytesWritten < $offset + $length)) {
try {
$content = $this->client->downloadPartial($this->formatPath($storageFile), $bytesWritten, $chunkSize);
} catch (Exception $e) {
DupLog::info('[DropboxAddon] Failed to download file: ' . $e->getMessage());
break;
}
if (file_put_contents($destFile, $content, FILE_APPEND) === false) {
DupLog::info("[DropboxAddon] Could not write to destination file. File: {$destFile}");
break;
}
$bytesWritten += strlen($content);
}
$this->client->setTimeout(0);
if ($bytesWritten === $offset) {
// nothing was downloaded
return false;
}
return $length > 0 ? $length : $bytesWritten - $offset;
}
}

View File

@@ -0,0 +1,449 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DropboxAddon\Models;
use Duplicator\Models\GlobalEntity;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Core\Views\TplMng;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\StorageAuthInterface;
use Duplicator\Utils\OAuth\TokenEntity;
use Duplicator\Utils\OAuth\TokenService;
use Exception;
/**
* @property DropboxAdapter $adapter
*/
class DropboxStorage extends AbstractStorageEntity implements StorageAuthInterface
{
/**
* Get default config
*
* @return array<string,scalar>
*/
protected static function getDefaultConfig(): array
{
$config = parent::getDefaultConfig();
return array_merge(
$config,
[
'v2_access_token' => '',
'authorized' => false,
'token_json' => false,
]
);
}
/**
* Get priority, used to sort storages.
* 100 is neutral value, 0 is the highest priority
*
* @return int
*/
public static function getPriority(): int
{
return 310;
}
/**
* Return the storage type
*
* @return int
*/
public static function getSType(): int
{
return 1;
}
/**
* Returns the storage type icon URL
*
* @return string Returns the storage icon URL
*/
public static function getStypeIconURL(): string
{
return DUPLICATOR_IMG_URL . '/dropbox.svg';
}
/**
* Returns the storage type name.
*
* @return string
*/
public static function getStypeName(): string
{
return __('Dropbox', 'duplicator-pro');
}
/**
* Get storage location string
*
* @return string
*/
public function getLocationString(): string
{
$dropBoxInfo = $this->getAccountInfo();
if (!isset($dropBoxInfo['locale']) || $dropBoxInfo['locale'] == 'en') {
return "https://dropbox.com/home/Apps/Duplicator%20Pro/" . ltrim($this->getStorageFolder(), '/');
} else {
return "https://dropbox.com/home";
}
}
/**
* 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
{
if (!$this->isAuthorized()) {
$errorMsg = __('Dropbox isn\'t authorized.', 'duplicator-pro');
return false;
}
return true;
}
/**
* Is autorized
*
* @return bool
*/
public function isAuthorized(): bool
{
return (bool) ($this->config['authorized'] ?? false);
}
/**
* Authorized from HTTP request
*
* @param string $message Message
*
* @return bool True if authorized, false if failed
*/
public function authorizeFromRequest(&$message = ''): bool
{
try {
if (($refreshToken = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'auth_code')) === '') {
throw new Exception(__('Authorization code is empty', 'duplicator-pro'));
}
$this->name = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'name', '');
$this->notes = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'notes', '');
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'max_packages', 10);
$this->config['storage_folder'] = self::getSanitizedInputFolder('storage_folder', 'remove');
$this->revokeAuthorization();
$token = new TokenEntity(self::getSType(), ['refresh_token' => $refreshToken]);
if ($token->refresh(true) === false) {
DupLog::infoTrace("Problem initializing Dropbox with {$refreshToken}");
throw new Exception(__("Couldn't connect. Dropbox access token is invalid or doesn't have required permissions.", 'duplicator-pro'));
}
/** @todo Config should contain scalar data but it was chosen to assign complex structures ignoring the type. this should be fixed sooner or later. */
$this->config['token_json'] = [ // @phpstan-ignore assign.propertyType
'refresh_token' => $token->getRefreshToken(),
'access_token' => $token->getAccessToken(),
'expires_in' => $token->getExpiresIn(),
'created' => $token->getCreated(),
'scope' => $token->getScope(),
];
$this->config['v2_access_token'] = $token->getAccessToken();
$this->config['authorized'] = true;
} catch (Exception $e) {
DupLog::trace("Problem authorizing Dropbox access token msg: " . $e->getMessage());
$message = $e->getMessage();
return false;
}
$message = __('Dropbox is connected successfully and Storage Provider Updated.', 'duplicator-pro');
return true;
}
/**
* Revokes authorization
*
* @param string $message Message
*
* @return bool True if authorized, false if failed
*/
public function revokeAuthorization(&$message = ''): bool
{
if (!$this->isAuthorized()) {
$message = __('Dropbox isn\'t authorized.', 'duplicator-pro');
return true;
}
try {
$client = $this->getAdapter()->getClient();
$client->revokeToken();
} catch (Exception $e) {
DupLog::trace("Problem revoking Dropbox access token msg: " . $e->getMessage());
} finally {
$this->config['v2_access_token'] = '';
$this->config['authorized'] = false;
$this->config['token_json'] = false;
}
$message = __('Dropbox is disconnected successfully.', 'duplicator-pro');
return true;
}
/**
* Get authorization URL
*
* @todo: This should be refactored to use the new TokenService class.
*
* @return string
*/
public function getAuthorizationUrl(): string
{
return (new TokenService(static::getSType()))->getRedirectUri();
}
/**
* Returns the config fields template data
*
* @return array<string, mixed>
*/
protected function getConfigFieldsData(): array
{
return array_merge($this->getDefaultConfigFieldsData(), [
'accountInfo' => $this->getAccountInfo(),
'quotaInfo' => $this->getQuota(),
]);
}
/**
* Returns the default config fields template data
*
* @return array<string, mixed>
*/
protected function getDefaultConfigFieldsData(): array
{
return [
'storage' => $this,
'accountInfo' => false,
'quotaInfo' => false,
'storageFolder' => $this->config['storage_folder'],
'maxPackages' => $this->config['max_packages'],
];
}
/**
* Returns the config fields template path
*
* @return string
*/
protected function getConfigFieldsTemplatePath(): string
{
return 'dropboxaddon/configs/dropbox';
}
/**
* 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, 'dropbox_max_files', 10);
$this->config['storage_folder'] = self::getSanitizedInputFolder('_dropbox_storage_folder', 'remove');
$message = sprintf(
__('Dropbox Storage Updated. Folder: %1$s', 'duplicator-pro'),
$this->getStorageFolder()
);
return true;
}
/**
* Get the storage adapter
*
* @return DropboxAdapter
*/
protected function getAdapter(): DropboxAdapter
{
if (! $this->adapter) {
$global = GlobalEntity::getInstance();
// if we have an oauth2 token, we may need to refresh the access token.
if (isset($this->config['token_json']) && is_array($this->config['token_json'])) {
$token = new TokenEntity(self::getSType(), $this->config['token_json']);
if ($token->isAboutToExpire()) {
if ($token->refresh()) {
/** @todo Config should contain scalar data but it was chosen to assign complex structures ignoring the type. this should be fixed sooner or later. */
$this->config['token_json'] = [ // @phpstan-ignore assign.propertyType
'refresh_token' => $token->getRefreshToken(),
'access_token' => $token->getAccessToken(),
'expires_in' => $token->getExpiresIn(),
'created' => $token->getCreated(),
'scope' => $token->getScope(),
];
$this->config['v2_access_token'] = $token->getAccessToken();
$this->save();
} else {
DupLog::infoTrace('Problem refreshing Dropbox token');
}
}
}
$this->adapter = new DropboxAdapter(
$this->config['v2_access_token'],
$this->getStorageFolder(),
!$global->ssl_disableverify,
($global->ssl_useservercerts ? '' : DUPLICATOR_CERT_PATH),
$global->ipv4_only
);
}
return $this->adapter;
}
/**
* Get account info
*
* @return false|array<string,mixed>
*/
protected function getAccountInfo()
{
if (!$this->isAuthorized()) {
return false;
}
try {
return $this->getAdapter()->getClient()->getAccountInfo();
} catch (Exception $e) {
DupLog::trace("Problem getting Dropbox account info. " . $e->getMessage());
}
return false;
}
/**
* Get dropbox quota
*
* @return false|array{used:int,total:int,perc:float,available:string}
*/
protected function getQuota()
{
if (!$this->isAuthorized()) {
return false;
}
$quota = $this->getAdapter()->getClient()->getQuota();
if (
!isset($quota['used']) ||
!isset($quota['allocation']['allocated']) ||
$quota['allocation']['allocated'] <= 0
) {
return false;
}
$quota_used = $quota['used'];
$quota_total = $quota['allocation']['allocated'];
$used_perc = round($quota_used * 100 / $quota_total, 1);
$available_quota = $quota_total - $quota_used;
$available_quota_str = size_format($available_quota) ?: 'unknown';
return [
'used' => $quota_used,
'total' => $quota_total,
'perc' => $used_perc,
'available' => $available_quota_str,
];
}
/**
* Get upload chunk size in bytes
*
* @return int bytes
*/
public function getUploadChunkSize(): int
{
$dGlobal = DynamicGlobalEntity::getInstance();
return $dGlobal->getValInt('dropbox_upload_chunksize_in_kb', 2000) * KB_IN_BYTES;
}
/**
* Get download chunk size in bytes
*
* @return int bytes
*/
public function getDownloadChunkSize(): int
{
$dGlobal = DynamicGlobalEntity::getInstance();
return $dGlobal->getValInt('dropbox_download_chunksize_in_kb', 10000) * KB_IN_BYTES;
}
/**
* 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);
}
/**
* @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 [
'dropbox_upload_chunksize_in_kb' => 2000,
'dropbox_download_chunksize_in_kb' => 10000,
];
}
/**
* @return void
*/
public static function renderGlobalOptions(): void
{
$dGlobal = DynamicGlobalEntity::getInstance();
TplMng::getInstance()->render(
'dropboxaddon/configs/global_options',
[
'uploadChunkSize' => $dGlobal->getValInt('dropbox_upload_chunksize_in_kb', 2000),
'downloadChunkSize' => $dGlobal->getValInt('dropbox_download_chunksize_in_kb', 10000),
]
);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Duplicator\Addons\DropboxAddon\Utils;
use Duplicator\Addons\DropboxAddon\DropboxAddon;
use Duplicator\Utils\AbstractAutoloader;
class Autoloader extends AbstractAutoloader
{
const VENDOR_PATH = DropboxAddon::ADDON_PATH . '/vendor-prefixed/';
/**
* Register autoloader function
*
* @return void
*/
public static function register(): void
{
spl_autoload_register([self::class, 'load']);
}
/**
* Load class
*
* @param string $className class name
*
* @return void
*/
public static function load($className): void
{
if (strpos($className, self::ROOT_VENDOR) === 0) {
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;
}
}
}
}
/**
* Return namespace mapping
*
* @return string[]
*/
protected static function getNamespacesVendorMapping(): array
{
return [
self::ROOT_VENDOR . 'GrahamCampbell\GuzzleFactory' => self::VENDOR_PATH . 'graham-campbell/guzzle-factory/src',
self::ROOT_VENDOR . 'Spatie\\Dropbox' => self::VENDOR_PATH . 'spatie/dropbox-api/src',
];
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Duplicator\Addons\DropboxAddon\Utils;
use Duplicator\Models\GlobalEntity;
use Duplicator\Utils\Logging\DupLog;
use VendorDuplicator\GrahamCampbell\GuzzleFactory\GuzzleFactory;
use VendorDuplicator\GuzzleHttp\Client as GuzzleClient;
use VendorDuplicator\GuzzleHttp\Psr7\Response;
use VendorDuplicator\Spatie\Dropbox\Client;
class DropboxClient extends Client
{
const OAUTH2_URL = 'https://www.dropbox.com/oauth2/';
/**
* Class contructor
*
* @param string $accessToken The access token
* @param GuzzleClient|null $client The HTTP client
* @param int $maxChunkSize The maximum size of a chunk
* @param int $maxUploadChunkRetries The maximum number of times to retry a chunk upload
* @param bool $sslVerify If true, use SSL
* @param string $sslCert If empty use server cert
* @param bool $ipv4Only If true, use IPv4 only
*/
public function __construct(
$accessToken,
?GuzzleClient $client = null,
int $maxChunkSize = self::MAX_CHUNK_SIZE,
int $maxUploadChunkRetries = 0,
$sslVerify = true,
$sslCert = '',
$ipv4Only = false
) {
if (!$client) {
$options = [];
if ($sslVerify === false) {
$verify = false;
} elseif (strlen($sslCert) === 0) {
$verify = true;
} else {
$verify = $sslCert;
}
$options['verify'] = $verify;
if ($ipv4Only) {
$options['force_ip_resolve'] = 'v4';
}
$options['handler'] = GuzzleFactory::handler();
$client = new GuzzleClient($options);
}
parent::__construct($accessToken, $client, $maxChunkSize, $maxUploadChunkRetries);
}
/**
* Use the app config to authenticate and get the access token
*
* @param string $auth_code The authorization code
* @param array{app_key: string, app_secret: string} $app_config The app config
*
* @return bool
*/
public function authenticate($auth_code, $app_config)
{
$url = self::OAUTH2_URL . 'token';
$args = $this->injectExtraReqArgs([
'timeout' => 30,
'body' => [
'client_id' => $app_config['app_key'],
'client_secret' => $app_config['app_secret'],
'code' => $auth_code,
'grant_type' => 'authorization_code',
],
]);
$response = wp_remote_post($url, $args);
if (is_wp_error($response)) {
DupLog::traceObject("Something wrong with while trying to get v2_access_token with code", $response);
return false;
}
DupLog::traceObject("Got v2 access_token", $response);
$ret_obj = json_decode($response['body']);
return $ret_obj->access_token ?? false;
}
/**
* Get the account's usage quota information.
*
* @return array{used: int, allocation: array{allocated: int}}|false
*/
public function getQuota()
{
try {
return $this->rpcEndpointRequest('users/get_space_usage');
} catch (\Exception $e) {
DupLog::trace('[DropboxClient] ' . $e->getMessage());
return false;
}
}
/**
* Set the timeout for the client
*
* @param int $timeout The timeout in seconds
*
* @return void
*/
public function setTimeout($timeout): void
{
if ($timeout > 0) {
$this->client = new GuzzleClient(['handler' => GuzzleFactory::handler(), 'timeout' => $timeout]);
} else {
$this->client = new GuzzleClient(['handler' => GuzzleFactory::handler()]);
}
}
/**
* Download a file from a user's Dropbox.
*
* @param string $path The path to download
* @param int $start The byte to start from
* @param int $length The number of bytes to download
*
* @return string
*
* @link https://www.dropbox.com/developers/documentation/http/documentation#files-download
*/
public function downloadPartial(string $path, int $start = 0, int $length = 1024 * 1024)
{
$arguments = ['path' => $this->normalizePath($path)];
$headers = ['Range' => "bytes=$start-" . ($start + $length - 1)];
/** @var Response $response */
$response = $this->contentEndpointRequestWithHeaders('files/download', $arguments, '', $headers);
return $response->getBody()->getContents();
}
/**
* Inject extra request arguments
*
* @param array<string, mixed> $opts The request options
*
* @return array<string, mixed>
*/
private function injectExtraReqArgs(array $opts): array
{
$global = GlobalEntity::getInstance();
$opts['sslverify'] = !$global->ssl_disableverify;
if (!$global->ssl_useservercerts) {
$opts['sslcertificates'] = DUPLICATOR_CERT_PATH;
}
return $opts;
}
/**
* Content endpoint request with custom headers
*
* @param string $endpoint The endpoint to send the request to
* @param array<string, mixed> $arguments The params to send.
* @param string $body The body of the request
* @param array<string, string> $headers Custom headers to add.
*
* @return \VendorDuplicator\Psr\Http\Message\ResponseInterface
*
* @throws \Exception
*/
public function contentEndpointRequestWithHeaders(string $endpoint, array $arguments, string $body = '', array $headers = [])
{
$headers['Dropbox-API-Arg'] = \json_encode($arguments);
if ($body !== '') {
$headers['Content-Type'] = 'application/octet-stream';
}
return $this->client->post($this->getEndpointUrl('content', $endpoint), ['headers' => $this->getHeaders($headers), 'body' => $body]);
}
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* Duplicator messages sections
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
use Duplicator\Addons\DropboxAddon\Models\DropboxStorage;
use Duplicator\Views\UI\UiDialog;
defined("ABSPATH") or die("");
/**
* Variables
*
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var \Duplicator\Core\Views\TplMng $tplMng
* @var array<string, mixed> $tplData
* @var DropboxStorage $storage
*/
$storage = $tplData["storage"];
/** @var false|object */
$accountInfo = $tplData["accountInfo"];
/** @var false|array{used:int,total:int,perc:float,available:string} */
$quotaInfo = $tplData["quotaInfo"];
/** @var string */
$storageFolder = $tplData["storageFolder"];
/** @var int */
$maxPackages = $tplData["maxPackages"];
$tplMng->render('admin_pages/storages/parts/provider_head');
?>
<tr>
<th scope="row"><label><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
<td>
<div class="authorization-state" id="state-unauthorized">
<!-- CONNECT -->
<button id="dupli-dropbox-connect-btn" type="button" class="button secondary hollow">
<i class="fa fa-plug"></i> <?php esc_html_e('Connect to Dropbox', 'duplicator-pro'); ?>
<img
src="<?php echo esc_url(DUPLICATOR_IMG_URL . '/dropbox.svg'); ?>"
style='vertical-align: middle; margin:-2px 0 0 3px; height:18px; width:18px'>
</button>
</div>
<div class="authorization-state" id="state-waiting-for-request-token">
<div style="padding:10px">
<i class="fas fa-circle-notch fa-spin"></i> <?php esc_html_e('Getting Dropbox request token...', 'duplicator-pro'); ?>
</div>
</div>
<div class="authorization-state" id="state-waiting-for-auth-button-click">
<!-- STEP 2 -->
<div class="storage-auth-step">
<p>
<b><?php esc_html_e("Step 1:", 'duplicator-pro'); ?></b>&nbsp;
<?php esc_html_e(' Duplicator needs to authorize at the Dropbox.com website.', 'duplicator-pro'); ?>
</p>
<div class="auth-code-popup-note">
<?php esc_html_e(
'Note: Clicking the button below will open a new tab/window. Please be sur e your browser does not block popups.',
'duplicator-pro'
); ?>
<?php esc_html_e(
'If a new tab/window does not open check your browsers address bar to allow popups from this URL.',
'duplicator-pro'
); ?>
</div>
<button
id="auth-redirect"
type="button"
class="button secondary hollow margin-bottom-0"
onclick="DupliJs.Storage.Dropbox.OpenAuthPage(); return false;">
<i class="fa fa-user"></i> <?php esc_html_e('Authorize Dropbox', 'duplicator-pro'); ?>
</button>
</div>
<div id="dropbox-auth-code-area" class="storage-auth-step">
<p>
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>
<?php esc_html_e("Paste code from Dropbox authorization page.", 'duplicator-pro'); ?>
</p>
<input style="width:400px" id="dropbox-auth-code" name="dropbox-auth-code" type="text" />
</div>
<!-- STEP 3 -->
<div class="storage-auth-step">
<p>
<b><?php esc_html_e("Step 3:", 'duplicator-pro'); ?></b>&nbsp;
<?php esc_html_e('Finalize Dropbox validation by clicking the "Finalize Setup" button.', 'duplicator-pro'); ?><br>
</p>
<button id="dropbox-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 class="authorization-state" id="state-waiting-for-access-token">
<div>
<i class="fas fa-circle-notch fa-spin"></i>
<?php esc_html_e('Performing final authorization...Please wait', 'duplicator-pro'); ?>
</div>
</div>
<div class="authorization-state" id="state-authorized" style="margin-top:-5px">
<?php if ($storage->isAuthorized() && is_array($accountInfo)) : ?>
<h3>
<?php esc_html_e('Dropbox Account', 'duplicator-pro'); ?><br />
<i class="dupli-edit-info">
<?php esc_html_e('Duplicator has been authorized to access this user\'s Dropbox account', 'duplicator-pro'); ?>
</i>
</h3>
<div id="dropbox-account-info">
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
<?php echo esc_html($accountInfo['name']['display_name']); ?><br />
<label><?php esc_html_e('Email', 'duplicator-pro'); ?>:</label>
<?php echo esc_html($accountInfo['email']); ?>
<?php if ($quotaInfo) { ?>
<br />
<label><?php esc_html_e('Quota Usage', 'duplicator-pro'); ?>:</label>
<?php
printf(
esc_html__('%1$s%% used, %2$s available', 'duplicator-pro'),
(int) $quotaInfo['perc'],
esc_html($quotaInfo['available'])
);
}
?>
</div>
<?php endif; ?>
<br />
<button type="button" class="button secondary hollow" onclick='DupliJs.Storage.Dropbox.CancelAuthorization();'>
<?php esc_html_e('Cancel Authorization', 'duplicator-pro'); ?>
</button><br />
<i class="dupli-edit-info">
<?php esc_html_e('Disassociates storage provider with the Dropbox account. Will require re-authorization.', 'duplicator-pro'); ?>
</i>
</div>
</td>
</tr>
<tr>
<th scope="row"><label for="_dropbox_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
<td>
<div class="horizontal-input-row">
<b>//Dropbox/Apps/Duplicator Pro/</b>
<input
id="_dropbox_storage_folder"
name="_dropbox_storage_folder"
type="text"
value="<?php echo esc_attr($storageFolder); ?>"
class="dupli-storeage-folder-path" />
</div>
<p>
<i>
<?php
esc_html_e(
"Folder where backups will be stored. This should be unique for each web-site using Duplicator.",
'duplicator-pro'
); ?>
</i>
</p>
</td>
</tr>
<tr>
<th scope="row"><label for=""><?php esc_html_e("Max Backups", 'duplicator-pro'); ?></label></th>
<td>
<div class="horizontal-input-row">
<input
id="dropbox_max_files"
name="dropbox_max_files"
type="number"
value="<?php echo (int) $maxPackages; ?>"
min="0"
maxlength="4"
data-parsley-errors-container="#dropbox_max_files_error_container"
data-parsley-required="true"
data-parsley-type="number"
data-parsley-min="0">
<label for="dropbox_max_files">
<?php esc_html_e("Number of Backups to keep in folder.", 'duplicator-pro'); ?> <br />
</label>
</div>
<?php $tplMng->render('admin_pages/storages/parts/max_backups_description'); ?>
<div id="dropbox_max_files_error_container" class="duplicator-error-container"></div>
</td>
</tr>
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
<?php
$alertConnStatus = new UiDialog();
$alertConnStatus->title = __('Dropbox Connection Status', 'duplicator-pro');
$alertConnStatus->message = ''; // javascript inserted message
$alertConnStatus->initAlert();
?>
<script>
jQuery(document).ready(function($) {
// DROPBOX RELATED METHODS
DupliJs.Storage.Dropbox.AuthorizationStates = {
UNAUTHORIZED: 0,
WAITING_FOR_REQUEST_TOKEN: 1,
WAITING_FOR_AUTH_BUTTON_CLICK: 2,
WAITING_FOR_ACCESS_TOKEN: 3,
AUTHORIZED: 4
}
DupliJs.Storage.Dropbox.authorizationState = <?php echo ($storage->isAuthorized() ? 4 : 0); ?>;
DupliJs.Storage.Dropbox.CancelAuthorization = function() {
DupliJs.Storage.RevokeAuth(<?php echo (int) $storage->getId(); ?>);
}
DupliJs.Storage.Dropbox.DropboxGetAuthUrl = function() {
DupliJs.Storage.Dropbox.AuthUrl = <?php echo json_encode($storage->getAuthorizationUrl()); ?>;
jQuery("#state-waiting-for-auth-button-click").show();
};
DupliJs.Storage.Dropbox.TransitionAuthorizationState = function(newState) {
jQuery('.authorization-state').hide();
jQuery('.dropbox_access_type').prop('disabled', true);
jQuery('.button_dropbox_test').prop('disabled', true);
switch (newState) {
case DupliJs.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED:
jQuery('.dropbox_access_type').prop('disabled', false);
$("#dropbox_authorization_state").val(DupliJs.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED);
DupliJs.Storage.Dropbox.requestToken = null;
jQuery("#state-unauthorized").show();
break;
case DupliJs.Storage.Dropbox.AuthorizationStates.WAITING_FOR_REQUEST_TOKEN:
DupliJs.Storage.Dropbox.GetRequestToken();
jQuery("#state-waiting-for-request-token").show();
break;
case DupliJs.Storage.Dropbox.AuthorizationStates.WAITING_FOR_AUTH_BUTTON_CLICK:
// Nothing to do here other than show the button and wait
jQuery("#state-waiting-for-auth-button-click").show();
break;
case DupliJs.Storage.Dropbox.AuthorizationStates.WAITING_FOR_ACCESS_TOKEN:
jQuery("#state-waiting-for-access-token").show();
if (DupliJs.Storage.Dropbox.requestToken != null) {
DupliJs.Storage.Dropbox.GetAccessToken();
} else {
<?php $alertConnStatus->showAlert(); ?>
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
"<?php esc_html_e('Tried transitioning to auth button click but don\'t have the request token!', 'duplicator-pro'); ?>";
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
DupliJs.Storage.Dropbox.TransitionAuthorizationState(DupliJs.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED);
}
break;
case DupliJs.Storage.Dropbox.AuthorizationStates.AUTHORIZED:
var token = $("#dropbox_access_token").val();
var token_secret = $("#dropbox_access_token_secret").val();
DupliJs.Storage.Dropbox.accessToken = {};
DupliJs.Storage.Dropbox.accessToken.t = token;
DupliJs.Storage.Dropbox.accessToken.s = token_secret;
jQuery("#state-authorized").show();
jQuery('.button_dropbox_test').prop('disabled', false);
break;
}
DupliJs.Storage.Dropbox.authorizationState = newState;
}
DupliJs.Storage.Dropbox.OpenAuthPage = function() {
window.open(DupliJs.Storage.Dropbox.AuthUrl, '_blank');
}
$("#dupli-dropbox-connect-btn").click(function(event) {
event.stopPropagation();
$(this).hide();
DupliJs.Storage.Dropbox.DropboxGetAuthUrl();
});
$('#dropbox-finalize-setup').click(function(event) {
event.stopPropagation();
if ($('#dropbox-auth-code').val().length > 5) {
DupliJs.Storage.PrepareForSubmit();
//$("#dup-storage-form").submit();
DupliJs.Storage.Authorize(
<?php echo (int) $storage->getId(); ?>,
<?php echo (int) $storage->getSType(); ?>, {
'name': $('#name').val(),
'notes': $('#notes').val(),
'storage_folder': $('#_dropbox_storage_folder').val(),
'max_packages': $('#dropbox_max_files').val(),
'auth_code': $('#dropbox-auth-code').val()
}
);
} else {
<?php $alertConnStatus->showAlert(); ?>
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
"<?php esc_html_e('Please enter your Dropbox authorization code!', 'duplicator-pro'); ?>";
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
}
return false;
});
DupliJs.Storage.Dropbox.TransitionAuthorizationState(DupliJs.Storage.Dropbox.authorizationState);
$('button#auth-validate').prop('disabled', true);
});
</script>

View File

@@ -0,0 +1,69 @@
<?php
/**
* Duplicator messages sections
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
use Duplicator\Addons\DropboxAddon\Models\DropboxStorage;
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(DropboxStorage::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="dropbox_upload_chunksize_in_kb"
id="dropbox_upload_chunksize_in_kb"
type="number"
min="100"
data-parsley-required
data-parsley-type="number"
data-parsley-errors-container="#dropbox_upload_chunksize_in_kb_error_container"
value="<?php echo (int) $tplData['uploadChunkSize']; ?>"
>&nbsp;<b>KB</b>
<div id="dropbox_upload_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
<p class="description">
<?php esc_html_e('How much should be uploaded to Dropbox per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
</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="dropbox_download_chunksize_in_kb"
id="dropbox_download_chunksize_in_kb"
type="number"
min="100"
data-parsley-required
data-parsley-type="number"
data-parsley-errors-container="#dropbox_download_chunksize_in_kb_error_container"
value="<?php echo (int) $tplData['downloadChunkSize']; ?>"
>&nbsp;<b>KB</b>
<div id="dropbox_download_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
<p class="description">
<?php esc_html_e('How much should be downloaded from Dropbox per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
</p>
</div>
</div>
</div>