first commit
This commit is contained in:
@@ -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__;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
<?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>
|
||||
<?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>
|
||||
@@ -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']; ?>"
|
||||
> <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']; ?>"
|
||||
> <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>
|
||||
Reference in New Issue
Block a user