502 lines
11 KiB
PHP
502 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* @package FOF
|
|
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace FOF40\Download;
|
|
|
|
defined('_JEXEC') || die;
|
|
|
|
use FOF40\Container\Container;
|
|
use FOF40\Download\Exception\DownloadError;
|
|
use FOF40\Timer\Timer;
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
class Download
|
|
{
|
|
/**
|
|
* The component container object
|
|
*
|
|
* @var Container
|
|
*/
|
|
protected $container;
|
|
|
|
/**
|
|
* Parameters passed from the GUI when importing from URL
|
|
*
|
|
* @var array
|
|
*/
|
|
private $params = [];
|
|
|
|
/**
|
|
* The download adapter which will be used by this class
|
|
*
|
|
* @var DownloadInterface
|
|
*/
|
|
private $adapter;
|
|
|
|
/**
|
|
* Additional params that will be passed to the adapter while performing the download
|
|
*
|
|
* @var array
|
|
*/
|
|
private $adapterOptions = [];
|
|
|
|
/**
|
|
* Public constructor
|
|
*
|
|
* @param Container $c The component container
|
|
*/
|
|
public function __construct(Container $c)
|
|
{
|
|
$this->container = $c;
|
|
|
|
// Find the best fitting adapter
|
|
$allAdapters = self::getFiles(__DIR__ . '/Adapter', [], ['AbstractAdapter.php']);
|
|
$priority = 0;
|
|
|
|
foreach ($allAdapters as $adapterInfo)
|
|
{
|
|
/** @var Adapter\AbstractAdapter $adapter */
|
|
$adapter = new $adapterInfo['classname'];
|
|
|
|
if (!$adapter->isSupported())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($adapter->priority > $priority)
|
|
{
|
|
$this->adapter = $adapter;
|
|
$priority = $adapter->priority;
|
|
}
|
|
}
|
|
|
|
// Load the language strings
|
|
$c->platform->loadTranslations('lib_fof40');
|
|
}
|
|
|
|
/**
|
|
* This method will crawl a starting directory and get all the valid files
|
|
* that will be analyzed by __construct. Then it organizes them into an
|
|
* associative array.
|
|
*
|
|
* @param string $path Folder where we should start looking
|
|
* @param array $ignoreFolders Folder ignore list
|
|
* @param array $ignoreFiles File ignore list
|
|
*
|
|
* @return array Associative array, where the `fullpath` key contains the path to the file,
|
|
* and the `classname` key contains the name of the class
|
|
*/
|
|
protected static function getFiles(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
|
|
{
|
|
$return = [];
|
|
|
|
$files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles);
|
|
|
|
// Ok, I got the files, now I have to organize them
|
|
foreach ($files as $file)
|
|
{
|
|
$clean = str_replace($path, '', $file);
|
|
$clean = trim(str_replace('\\', '/', $clean), '/');
|
|
|
|
$parts = explode('/', $clean);
|
|
|
|
$return[] = [
|
|
'fullpath' => $file,
|
|
'classname' => '\\FOF40\\Download\\Adapter\\' . ucfirst(basename($parts[0], '.php')),
|
|
];
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Recursive function that will scan every directory unless it's in the
|
|
* ignore list. Files that aren't in the ignore list are returned.
|
|
*
|
|
* @param string $path Folder where we should start looking
|
|
* @param array $ignoreFolders Folder ignore list
|
|
* @param array $ignoreFiles File ignore list
|
|
*
|
|
* @return array List of all the files
|
|
*/
|
|
protected static function scanDirectory(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
|
|
{
|
|
$return = [];
|
|
|
|
$handle = @opendir($path);
|
|
|
|
if (!$handle)
|
|
{
|
|
return $return;
|
|
}
|
|
|
|
while (($file = readdir($handle)) !== false)
|
|
{
|
|
if ($file == '.' || $file == '..')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$fullpath = $path . '/' . $file;
|
|
|
|
if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (is_dir($fullpath))
|
|
{
|
|
$return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return);
|
|
}
|
|
else
|
|
{
|
|
$return[] = $path . '/' . $file;
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Forces the use of a specific adapter
|
|
*
|
|
* @param string $className The name of the class or the name of the adapter
|
|
*/
|
|
public function setAdapter(?string $className = null): void
|
|
{
|
|
if (is_null($className))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$adapter = null;
|
|
|
|
if (class_exists($className, true))
|
|
{
|
|
$adapter = new $className;
|
|
}
|
|
elseif (class_exists('\\FOF40\\Download\\Adapter\\' . ucfirst($className)))
|
|
{
|
|
$className = '\\FOF40\\Download\\Adapter\\' . ucfirst($className);
|
|
$adapter = new $className;
|
|
}
|
|
|
|
if (!is_object($adapter))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!$adapter instanceof DownloadInterface)
|
|
{
|
|
return;
|
|
}
|
|
|
|
$this->adapter = $adapter;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the current adapter
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAdapterName(): string
|
|
{
|
|
if (is_object($this->adapter))
|
|
{
|
|
$class = get_class($this->adapter);
|
|
|
|
return strtolower(str_ireplace('FOF40\\Download\\Adapter\\', '', $class));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Returns the additional options for the adapter
|
|
*
|
|
* @return array
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function getAdapterOptions(): array
|
|
{
|
|
return $this->adapterOptions;
|
|
}
|
|
|
|
/**
|
|
* Sets the additional options for the adapter
|
|
*
|
|
* @param array $options
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function setAdapterOptions(array $options): void
|
|
{
|
|
$this->adapterOptions = $options;
|
|
}
|
|
|
|
/**
|
|
* Download data from a URL and return it.
|
|
*
|
|
* Important note about ranges: byte ranges start at 0. This means that the first 500 bytes of a file are from 0
|
|
* to 499, NOT from 1 to 500. If you ask more bytes than there are in the file or a range which is invalid or does
|
|
* not exist this method will return false.
|
|
*
|
|
* @param string $url The URL to download from
|
|
* @param int $from Byte range to start downloading from. Use null (default) for start of file.
|
|
* @param int $to Byte range to stop downloading. Use null to download the entire file ($from will be
|
|
* ignored!)
|
|
*
|
|
* @return string The downloaded data or null on failure
|
|
*/
|
|
public function getFromURL(string $url, ?int $from = null, ?int $to = null): ?string
|
|
{
|
|
try
|
|
{
|
|
return $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
|
|
}
|
|
catch (DownloadError $e)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs the staggered download of file.
|
|
*
|
|
* @param array $params A parameters array, as sent by the user interface
|
|
*
|
|
* @return array A return status array
|
|
*/
|
|
public function importFromURL(array $params): array
|
|
{
|
|
$this->params = $params;
|
|
|
|
// Fetch data
|
|
$url = $this->getParam('url');
|
|
$localFilename = $this->getParam('localFilename');
|
|
$frag = $this->getParam('frag', -1);
|
|
$totalSize = $this->getParam('totalSize', -1);
|
|
$doneSize = $this->getParam('doneSize', -1);
|
|
$maxExecTime = $this->getParam('maxExecTime', 5);
|
|
$runTimeBias = $this->getParam('runTimeBias', 75);
|
|
$length = $this->getParam('length', 1048576);
|
|
|
|
if (empty($localFilename))
|
|
{
|
|
$localFilename = basename($url);
|
|
|
|
if (strpos($localFilename, '?') !== false)
|
|
{
|
|
$paramsPos = strpos($localFilename, '?');
|
|
$localFilename = substr($localFilename, 0, $paramsPos - 1);
|
|
|
|
$platformBaseDirectories = $this->container->platform->getPlatformBaseDirs();
|
|
$tmpDir = $platformBaseDirectories['tmp'];
|
|
$tmpDir = rtrim($tmpDir, '/\\');
|
|
|
|
$localFilename = $tmpDir . '/' . $localFilename;
|
|
}
|
|
}
|
|
|
|
// Init retArray
|
|
$retArray = [
|
|
"status" => true,
|
|
"error" => '',
|
|
"frag" => $frag,
|
|
"totalSize" => $totalSize,
|
|
"doneSize" => $doneSize,
|
|
"percent" => 0,
|
|
"localfile" => $localFilename,
|
|
];
|
|
|
|
try
|
|
{
|
|
$timer = new Timer($maxExecTime, $runTimeBias);
|
|
$start = $timer->getRunningTime(); // Mark the start of this download
|
|
$break = false; // Don't break the step
|
|
|
|
do
|
|
{
|
|
// Do we have to initialize the file?
|
|
if ($frag == -1)
|
|
{
|
|
// Currently downloaded size
|
|
$doneSize = 0;
|
|
|
|
if (@file_exists($localFilename))
|
|
{
|
|
@unlink($localFilename);
|
|
}
|
|
|
|
// Delete and touch the output file
|
|
$fp = @fopen($localFilename, 'w');
|
|
|
|
if ($fp !== false)
|
|
{
|
|
@fclose($fp);
|
|
}
|
|
|
|
// Init
|
|
$frag = 0;
|
|
|
|
$retArray['totalSize'] = $this->adapter->getFileSize($url);
|
|
|
|
if ($retArray['totalSize'] <= 0)
|
|
{
|
|
$retArray['totalSize'] = 0;
|
|
}
|
|
|
|
$totalSize = $retArray['totalSize'];
|
|
}
|
|
|
|
// Calculate from and length
|
|
$from = $frag * $length;
|
|
$to = $length + $from - 1;
|
|
|
|
// Try to download the first frag
|
|
$required_time = 1.0;
|
|
|
|
$error = '';
|
|
|
|
try
|
|
{
|
|
$result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
|
|
}
|
|
catch (DownloadError $e)
|
|
{
|
|
$result = false;
|
|
$error = $e->getMessage();
|
|
}
|
|
|
|
if ($result === false)
|
|
{
|
|
// Failed download
|
|
if ($frag == 0)
|
|
{
|
|
// Failure to download first frag = failure to download. Period.
|
|
$retArray['status'] = false;
|
|
$retArray['error'] = $error;
|
|
|
|
return $retArray;
|
|
}
|
|
else
|
|
{
|
|
// Since this is a staggered download, consider this normal and finish
|
|
$frag = -1;
|
|
$totalSize = $doneSize;
|
|
$break = true;
|
|
}
|
|
}
|
|
|
|
// Add the currently downloaded frag to the total size of downloaded files
|
|
if ($result !== false)
|
|
{
|
|
$fileSize = strlen($result);
|
|
$doneSize += $fileSize;
|
|
|
|
// Append the file
|
|
$fp = @fopen($localFilename, 'a');
|
|
|
|
if ($fp === false)
|
|
{
|
|
// Can't open the file for writing
|
|
$retArray['status'] = false;
|
|
$retArray['error'] = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $localFilename);
|
|
|
|
return $retArray;
|
|
}
|
|
|
|
fwrite($fp, $result);
|
|
fclose($fp);
|
|
|
|
$frag++;
|
|
|
|
if (($fileSize < $length) || ($fileSize > $length)
|
|
|| (($totalSize == $doneSize) && ($totalSize > 0))
|
|
)
|
|
{
|
|
// A partial download or a download larger than the frag size means we are done
|
|
$frag = -1;
|
|
//debugMsg("-- Import complete (partial download of last frag)");
|
|
$totalSize = $doneSize;
|
|
$break = true;
|
|
}
|
|
}
|
|
|
|
// Advance the frag pointer and mark the end
|
|
$end = $timer->getRunningTime();
|
|
|
|
// Do we predict that we have enough time?
|
|
$required_time = max(1.1 * ($end - $start), $required_time);
|
|
|
|
if ($required_time > (10 - $end + $start))
|
|
{
|
|
$break = true;
|
|
}
|
|
|
|
$start = $end;
|
|
|
|
} while (($timer->getTimeLeft() > 0) && !$break);
|
|
|
|
if ($frag == -1)
|
|
{
|
|
$percent = 100;
|
|
}
|
|
elseif ($doneSize <= 0)
|
|
{
|
|
$percent = 0;
|
|
}
|
|
elseif ($totalSize > 0)
|
|
{
|
|
$percent = 100 * ($doneSize / $totalSize);
|
|
}
|
|
else
|
|
{
|
|
$percent = 0;
|
|
}
|
|
|
|
// Update $retArray
|
|
$retArray = [
|
|
"status" => true,
|
|
"error" => '',
|
|
"frag" => $frag,
|
|
"totalSize" => $totalSize,
|
|
"doneSize" => $doneSize,
|
|
"percent" => $percent,
|
|
];
|
|
}
|
|
catch (DownloadError $e)
|
|
{
|
|
$retArray['status'] = false;
|
|
$retArray['error'] = $e->getMessage();
|
|
}
|
|
|
|
return $retArray;
|
|
}
|
|
|
|
/**
|
|
* Used to decode the $params array
|
|
*
|
|
* @param string $key The parameter key you want to retrieve the value for
|
|
* @param mixed $default The default value, if none is specified
|
|
*
|
|
* @return mixed The value for this parameter key
|
|
*/
|
|
private function getParam(string $key, $default = null)
|
|
{
|
|
if (array_key_exists($key, $this->params))
|
|
{
|
|
return $this->params[$key];
|
|
}
|
|
else
|
|
{
|
|
return $default;
|
|
}
|
|
}
|
|
}
|