fix download large file for map download

This commit is contained in:
Beu
2025-07-25 21:49:05 +02:00
parent 1fb7c87925
commit ee0b0ac166
6 changed files with 222 additions and 134 deletions

View File

@@ -36,6 +36,7 @@ class AsyncHttpRequest implements UsageInformationAble {
private $contentType = 'text/xml; charset=UTF-8;'; private $contentType = 'text/xml; charset=UTF-8;';
private $timeout = 60; private $timeout = 60;
private $headers = array(); private $headers = array();
private $handle = null;
public function __construct($maniaControl, $url) { public function __construct($maniaControl, $url) {
$this->maniaControl = $maniaControl; $this->maniaControl = $maniaControl;
@@ -57,8 +58,7 @@ class AsyncHttpRequest implements UsageInformationAble {
->set(CURLOPT_USERAGENT, 'ManiaControl v' . ManiaControl::VERSION)// user-agent ->set(CURLOPT_USERAGENT, 'ManiaControl v' . ManiaControl::VERSION)// user-agent
->set(CURLOPT_RETURNTRANSFER, true)// ->set(CURLOPT_RETURNTRANSFER, true)//
->set(CURLOPT_FOLLOWLOCATION, true)// support redirect ->set(CURLOPT_FOLLOWLOCATION, true)// support redirect
->set(CURLOPT_SSL_VERIFYPEER, false) ->set(CURLOPT_SSL_VERIFYPEER, false);
->set(CURLOPT_HEADER, true);
return $request; return $request;
} }
@@ -76,8 +76,14 @@ class AsyncHttpRequest implements UsageInformationAble {
array_push($this->headers, 'Accept-Charset: utf-8'); array_push($this->headers, 'Accept-Charset: utf-8');
$request = $this->newRequest($this->url, $this->timeout); $request = $this->newRequest($this->url, $this->timeout);
$request->getOptions()->set(CURLOPT_AUTOREFERER, true)// accept link reference $request->getOptions()
->set(CURLOPT_HTTPHEADER, $this->headers); // headers ->set(CURLOPT_AUTOREFERER, true)// accept link reference
->set(CURLOPT_HTTPHEADER, $this->headers); // headers
if ($this->handle !== null) {
$request->getOptions()
->set(CURLOPT_FILE, $this->handle);
}
$this->processRequest($request); $this->processRequest($request);
} }
@@ -287,4 +293,22 @@ class AsyncHttpRequest implements UsageInformationAble {
public function setTimeout($timeout) { public function setTimeout($timeout) {
$this->timeout = $timeout; $this->timeout = $timeout;
} }
/**
* Gets the File Resource handle
*
* @return int
*/
public function getHandle() {
return $this->handle;
}
/**
* Sets the File Resource handle
*
* @param int $handle
*/
public function setHandle($handle) {
$this->handle = $handle;
}
} }

View File

@@ -630,136 +630,155 @@ class DirectoryBrowser implements ManialinkPageAnswerListener {
$folderPath = $player->getCache($this, self::CACHE_FOLDER_PATH); $folderPath = $player->getCache($this, self::CACHE_FOLDER_PATH);
if (filter_var($url, FILTER_VALIDATE_URL)) { if (filter_var($url, FILTER_VALIDATE_URL)) {
$asyncHttpRequest = new AsyncHttpRequest($this->maniaControl, $url); try {
$asyncHttpRequest->setCallable(function ($file, $error, $headers) use ($url, $folderPath, $player) { $tempFile = tempnam(sys_get_temp_dir(), 'map_');
if (!$file || $error) { $fp = fopen($tempFile, 'w+');
$message = "Impossible to download the file: " . $error;
$this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error during the download of the zip file "'. $url .'": '. $error);
return;
}
$filePath = "";
$finfo = new finfo(FILEINFO_MIME_TYPE); $this->maniaControl->getChat()->sendSuccess('Starting download...', $player);
if ($finfo->buffer($file) === "application/zip") {
$zip = new ZipArchive();
// Create a temporary file $asyncHttpRequest = new AsyncHttpRequest($this->maniaControl, $url);
$tempFile = tempnam(sys_get_temp_dir(), 'zip'); $asyncHttpRequest->setHandle($fp);
file_put_contents($tempFile, $file); $asyncHttpRequest->setCallable(function ($file, $error, $headers) use ($url, $folderPath, $tempFile, $fp, $player) {
// closing file handle
$open = $zip->open($tempFile); fclose($fp);
if ($error) {
if ($open === true) { $message = "Impossible to download the file: " . $error;
$zip->extractTo($folderPath);
$zip->close();
$message = "Successfully extracted zip archive from ". $url;
$this->maniaControl->getChat()->sendSuccess($message, $player);
Logger::log(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') downloaded the zip file "'. $url .'"');
} else {
$message = "Cannot extract archive from ". $url;
$this->maniaControl->getChat()->sendError($message, $player); $this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloading the zip file "'. $url .'": Cannot extract the archive'); Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error during the download of the zip file "'. $url .'": '. $error);
return;
} }
// Clean up the temporary file $filePath = "";
unlink($tempFile);
} else {
$fileName = "";
$contentdispositionheader = ""; $finfo = new finfo(FILEINFO_MIME_TYPE);
foreach ($headers as $key => $value) {
if (strtolower($key) === "content-disposition") {
$contentdispositionheader = urldecode($value);
break;
}
}
if ($contentdispositionheader !== "") { if ($finfo->file($tempFile) === "application/zip") {
$value = $contentdispositionheader; $zip = new ZipArchive();
if (strpos($value, ';') !== false) { $open = $zip->open($tempFile);
list($type, $attr_parts) = explode(';', $value, 2); if ($open === true) {
$zip->extractTo($folderPath);
$attr_parts = explode(';', $attr_parts); $zip->close();
$attributes = array(); $message = "Successfully extracted zip archive from ". $url;
$this->maniaControl->getChat()->sendSuccess($message, $player);
foreach ($attr_parts as $part) { Logger::log(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') downloaded the zip file "'. $url .'"');
if (strpos($part, '=') === false) { } else {
continue; $message = "Cannot extract archive from ". $url;
}
list($key, $value) = explode('=', $part, 2);
$attributes[trim($key)] = trim($value);
}
$fileName = null;
if (array_key_exists('filename*', $attributes)) {
$fileName = trim($attributes['filename*']);
// remove prefix if needed
if (strpos(strtolower($fileName), "utf-8''") === 0) {
$fileName = substr($fileName, strlen("utf-8''"));
}
} else if (array_key_exists('filename', $attributes)) {
$fileName = trim($attributes['filename']);
}
if ($fileName !== null) {
if (substr($fileName, 0, 1) === '"' && substr($fileName, -1, 1) === '"') {
$fileName = substr($fileName, 1, -1);
}
$filePath = $folderPath . FileUtil::getClearedFileName($fileName);
}
}
if (!$this->isMapFileName($filePath)) {
$message = "File is not a map: " . $fileName;
$this->maniaControl->getChat()->sendError($message, $player); $this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloadeding the map file "'. $fileName .'": File is not a map'); Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloading the zip file "'. $url .'": Cannot extract the archive');
return;
} }
// Clean up the temporary file
unlink($tempFile);
} else { } else {
$path = parse_url($url, PHP_URL_PATH); $fileName = "";
// extracted basename $contentdispositionheader = "";
$fileName = basename($path); foreach ($headers as $key => $value) {
if (strtolower($key) === "content-disposition") {
if (!$this->isMapFileName($fileName)) { $contentdispositionheader = urldecode($value);
$fileName .= ".Map.Gbx"; break;
}
$filePath = $folderPath . $fileName;
}
if ($filePath != "") {
if (file_exists($filePath)) {
$index = 1;
while (file_exists(substr($filePath, 0, -8) . "-" . $index . ".Map.Gbx")) {
$index++;
} }
$filePath = substr($filePath, 0, -8) . "-" . $index . ".Map.Gbx";
}
$bytes = file_put_contents($filePath, $file);
if (!$bytes || $bytes <= 0) {
$message = "Failed to write file " . $filePath;
$this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloadeding the map file "'. $fileName .'": Failed to write the file');
return;
} }
$message = "Successfully downloaded the map ". $fileName; if ($contentdispositionheader !== "") {
$this->maniaControl->getChat()->sendSuccess($message, $player); $value = $contentdispositionheader;
Logger::log(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') downloaded the map file "'. $filePath .'"');
if (strpos($value, ';') !== false) {
list($type, $attr_parts) = explode(';', $value, 2);
$attr_parts = explode(';', $attr_parts);
$attributes = array();
foreach ($attr_parts as $part) {
if (strpos($part, '=') === false) {
continue;
}
list($key, $value) = explode('=', $part, 2);
$attributes[trim($key)] = trim($value);
}
$fileName = null;
if (array_key_exists('filename*', $attributes)) {
$fileName = trim($attributes['filename*']);
// remove prefix if needed
if (strpos(strtolower($fileName), "utf-8''") === 0) {
$fileName = substr($fileName, strlen("utf-8''"));
}
} else if (array_key_exists('filename', $attributes)) {
$fileName = trim($attributes['filename']);
}
if ($fileName !== null) {
if (substr($fileName, 0, 1) === '"' && substr($fileName, -1, 1) === '"') {
$fileName = substr($fileName, 1, -1);
}
$filePath = $folderPath . FileUtil::getClearedFileName($fileName);
}
}
if (!$this->isMapFileName($filePath)) {
$message = "File is not a map: " . $fileName;
$this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloadeding the map file "'. $fileName .'": File is not a map');
// Clean up the temporary file
unlink($tempFile);
return;
}
} else {
$path = parse_url($url, PHP_URL_PATH);
// extracted basename
$fileName = basename($path);
if (!$this->isMapFileName($fileName)) {
$fileName .= ".Map.Gbx";
}
$filePath = $folderPath . $fileName;
}
if ($filePath != "") {
if (file_exists($filePath)) {
$index = 1;
while (file_exists(substr($filePath, 0, -8) . "-" . $index . ".Map.Gbx")) {
$index++;
}
$filePath = substr($filePath, 0, -8) . "-" . $index . ".Map.Gbx";
}
$renamed = rename($tempFile, $filePath);
if (!$renamed) {
$message = "Failed to write file " . $filePath;
$this->maniaControl->getChat()->sendError($message, $player);
Logger::logError(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') encountered an error when downloadeding the map file "'. $fileName .'": Failed to write the file');
// Clean up the temporary file
unlink($tempFile);
return;
}
$message = "Successfully downloaded the map ". $fileName;
$this->maniaControl->getChat()->sendSuccess($message, $player);
Logger::log(AuthenticationManager::getAuthLevelName($player->authLevel) .' "'. $player->nickname . '" ('. $player->login .') downloaded the map file "'. $filePath .'"');
}
} }
}
$this->showManiaLink($player); $this->showManiaLink($player);
}); });
$asyncHttpRequest->getData(); $asyncHttpRequest->getData();
} catch (\Throwable $th) {
Logger::logError('Error when downloading file: '. $th->getMessage());
$this->maniaControl->getChat()->sendError('Error when downloading file: '. $th->getMessage(), $player);
}
} }
} }
} }

View File

@@ -47,8 +47,7 @@ abstract class WebReader {
->set(CURLOPT_USERAGENT, 'ManiaControl v' . ManiaControl::VERSION)// user-agent ->set(CURLOPT_USERAGENT, 'ManiaControl v' . ManiaControl::VERSION)// user-agent
->set(CURLOPT_RETURNTRANSFER, true)// return instead of output content ->set(CURLOPT_RETURNTRANSFER, true)// return instead of output content
->set(CURLOPT_AUTOREFERER, true)// follow redirects ->set(CURLOPT_AUTOREFERER, true)// follow redirects
->set(CURLOPT_SSL_VERIFYPEER, false) ->set(CURLOPT_SSL_VERIFYPEER, false);
->set(CURLOPT_HEADER, true);;
return $request; return $request;
} }

View File

@@ -6,7 +6,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
class Request extends EventDispatcher implements RequestInterface class Request extends EventDispatcher implements RequestInterface
{ {
/** /**
* @var resource cURL handler * @var \CurlHandle cURL handler
*/ */
protected $ch; protected $ch;
@@ -20,6 +20,11 @@ class Request extends EventDispatcher implements RequestInterface
*/ */
protected $options = null; protected $options = null;
/**
* @var array Response Headers feed by the CURLOPT_HEADERFUNCTION callback
*/
protected $responseHeaders;
/** /**
* Create new cURL handle * Create new cURL handle
* *
@@ -30,6 +35,35 @@ class Request extends EventDispatcher implements RequestInterface
if ($url !== null) { if ($url !== null) {
$this->getOptions()->set(CURLOPT_URL, $url); $this->getOptions()->set(CURLOPT_URL, $url);
} }
$this->getOptions()->set(CURLOPT_HEADERFUNCTION, function($ch, $headerLine) {
$len = strlen($headerLine);
// Handle HTTP status lines (e.g., HTTP/1.1 200 OK)
if (preg_match('/^HTTP\/\d\.\d\s+\d+/', $headerLine)) {
$this->responseHeaders = []; // Reset on redirect or multiple responses
}
$parts = explode(':', $headerLine, 2);
if (count($parts) === 2) {
$key = strtolower(trim($parts[0]));
$value = trim($parts[1]);
// Handle multiple headers with same name
if (!isset($this->responseHeaders[$key])) {
$this->responseHeaders[$key] = $value;
} else {
if (is_array($this->responseHeaders[$key])) {
$this->responseHeaders[$key][] = $value;
} else {
$this->responseHeaders[$key] = [$this->responseHeaders[$key], $value];
}
}
}
return $len;
});
$this->ch = curl_init(); $this->ch = curl_init();
} }
@@ -72,7 +106,7 @@ class Request extends EventDispatcher implements RequestInterface
/** /**
* Returns cURL raw resource * Returns cURL raw resource
* *
* @return resource cURL handle * @return \CurlHandle cURL handle
*/ */
public function getHandle() public function getHandle()
{ {
@@ -90,6 +124,16 @@ class Request extends EventDispatcher implements RequestInterface
return (int)$this->ch; return (int)$this->ch;
} }
/**
* Get the response headers
*
* @return array
*/
public function getResponseHeaders()
{
return $this->responseHeaders;
}
/** /**
* Perform a cURL session. * Perform a cURL session.
* Equivalent to curl_exec(). * Equivalent to curl_exec().
@@ -108,6 +152,7 @@ class Request extends EventDispatcher implements RequestInterface
$content = curl_exec($this->ch); $content = curl_exec($this->ch);
$response = new Response($this, $content); $response = new Response($this, $content);
$response->setHeaders($this->responseHeaders);
$errorCode = curl_errno($this->ch); $errorCode = curl_errno($this->ch);
if ($errorCode !== CURLE_OK) { if ($errorCode !== CURLE_OK) {
$response->setError(new Error(curl_error($this->ch), $errorCode)); $response->setError(new Error(curl_error($this->ch), $errorCode));

View File

@@ -11,7 +11,7 @@ class RequestsQueue extends EventDispatcher implements RequestsQueueInterface, \
protected $defaultOptions = null; protected $defaultOptions = null;
/** /**
* @var resource cURL multi handler * @var \CurlMultiHandle cURL multi handler
*/ */
protected $mh; protected $mh;
@@ -115,6 +115,7 @@ class RequestsQueue extends EventDispatcher implements RequestsQueueInterface, \
$event = new Event(); $event = new Event();
$event->request = $request; $event->request = $request;
$event->response = new Response($request, curl_multi_getcontent($request->getHandle())); $event->response = new Response($request, curl_multi_getcontent($request->getHandle()));
$event->response->setHeaders($event->request->getResponseHeaders());
if ($result !== CURLE_OK) { if ($result !== CURLE_OK) {
$event->response->setError(new Error(curl_error($request->getHandle()), $result)); $event->response->setError(new Error(curl_error($request->getHandle()), $result));
} }

View File

@@ -19,18 +19,7 @@ class Response
$this->ch = $request->getHandle(); $this->ch = $request->getHandle();
if ($content != null) { if ($content != null) {
$header_size = $this->getInfo(CURLINFO_HEADER_SIZE); $this->content = $content;
foreach (explode("\r\n", substr($content, 0, $header_size)) as $value) {
if(false !== ($matches = explode(':', $value, 2))) {
if (count($matches) === 2) {
$headers_arr["{$matches[0]}"] = trim($matches[1]);
}
}
}
$this->headers = $headers_arr;
$this->content = substr($content, $header_size);
} }
} }
@@ -89,6 +78,17 @@ class Response
return isset($this->error); return isset($this->error);
} }
/**
* Sets headers
*
* @param array $error headers to set
* @return void
*/
public function setHeaders(array $headers)
{
$this->headers = $headers;
}
/** /**
* Returns headers of request * Returns headers of request
* *