Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 102 additions & 64 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class LogException extends \Exception {
}


use CurlHandle;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is unnecessary. Saw a report on the forum:

Warning: The use statement with non-compound name ‘CurlHandle’ has no effect in [...]/updater/index.php on line 30

Maybe \CurlHandle on the type hint for getCurl() too?


class Updater {
private string $nextcloudDir;
private array $configValues = [];
Expand Down Expand Up @@ -517,20 +519,7 @@ private function getUpdateServerResponse(): array {
$this->silentLog('[info] updateURL: ' . $updateURL);

// Download update response
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_URL => $updateURL,
CURLOPT_USERAGENT => 'Nextcloud Updater',
]);

if ($this->getConfigOption('proxy') !== null) {
curl_setopt_array($curl, [
CURLOPT_PROXY => $this->getConfigOptionString('proxy'),
CURLOPT_PROXYUSERPWD => $this->getConfigOptionString('proxyuserpwd'),
CURLOPT_HTTPPROXYTUNNEL => $this->getConfigOption('proxy') ? 1 : 0,
]);
}
$curl = $this->getCurl($updateURL);

/** @var false|string $response */
$response = curl_exec($curl);
Expand Down Expand Up @@ -568,16 +557,10 @@ private function getUpdateServerResponse(): array {
public function downloadUpdate(): void {
$this->silentLog('[info] downloadUpdate()');

$response = $this->getUpdateServerResponse();
if (!isset($response['url']) || !is_string($response['url'])) {
throw new \Exception('Response from update server is missing url');
}
$downloadURLs = $this->getDownloadURLs();
$this->silentLog('[info] will try to download archive from: ' . implode(', ', $downloadURLs));

$storageLocation = $this->getUpdateDirectoryLocation() . '/updater-' . $this->getConfigOptionMandatoryString('instanceid') . '/downloads/';
$saveLocation = $storageLocation . basename($response['url']);
$this->previousProgress = 0;

$ch = curl_init($response['url']);

if (!file_exists($storageLocation)) {
$state = mkdir($storageLocation, 0750, true);
Expand All @@ -592,21 +575,61 @@ public function downloadUpdate(): void {
$this->silentLog('[info] extracted Archive location exists');
$this->recursiveDelete($storageLocation . 'nextcloud/');
}
// see if there's an existing incomplete download to resume
if (is_file($saveLocation)) {
$size = filesize($saveLocation);
$range = $size . '-';
curl_setopt($ch, CURLOPT_RANGE, $range);
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
}

foreach ($downloadURLs as $url) {
$this->previousProgress = 0;
$saveLocation = $storageLocation . basename($url);
if ($this->downloadArchive($url, $saveLocation)) {
return;
}
}

$fp = fopen($saveLocation, 'a');
throw new \Exception('All downloads failed. See updater logs for more information.');
}

private function getDownloadURLs(): array {
$response = $this->getUpdateServerResponse();
$downloadURLs = [];
if (!isset($response['downloads']) || !is_array($response['downloads'])) {
if (isset($response['url']) && is_string($response['url'])) {
// Compatibility with previous verison of updater_server
$ext = pathinfo($response['url'], PATHINFO_EXTENSION);
$response['downloads'] = [
$ext => [$response['url']]
];
} else {
throw new \Exception('Response from update server is missing download URLs');
}
}
foreach ($response['downloads'] as $format => $urls) {
if (!$this->isAbleToDecompress($format)) {
continue;
}
foreach ($urls as $url) {
if (!is_string($url)) {
continue;
}
$downloadURLs[] = $url;
}
}

if (empty($downloadURLs)) {
throw new \Exception('Your PHP install is not able to decompress any archive. Try to install modules like zip or bzip.');
}

return array_unique($downloadURLs);

}

private function getCurl(string $url): CurlHandle {
$ch = curl_init($url);
if ($ch === false) {
throw new \Exception('Fail to open cUrl handler');
}

curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Nextcloud Updater',
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_MAXREDIRS => 2,
Expand All @@ -620,47 +643,61 @@ public function downloadUpdate(): void {
]);
}

return $ch;
}

private function downloadArchive(string $fromUrl, string $toLocation): bool {
$ch = $this->getCurl($fromUrl);

// see if there's an existing incomplete download to resume
if (is_file($toLocation)) {
$size = (int)filesize($toLocation);
$range = $size . '-';
curl_setopt($ch, CURLOPT_RANGE, $range);
$this->silentLog('[info] previous download found; resuming from ' . $this->formatBytes($size));
}

$fp = fopen($toLocation, 'ab');
if ($fp === false) {
throw new \Exception('Fail to open file in ' . $toLocation);
}

curl_setopt_array($ch, [
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => [$this, 'downloadProgressCallback'],
CURLOPT_FILE => $fp,
]);

if (curl_exec($ch) === false) {
throw new \Exception('Curl error: ' . curl_error($ch));
}

$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200 && $httpCode !== 206) {
$statusCodes = [
400 => 'Bad request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
];

$message = 'Download failed';
if (is_int($httpCode) && isset($statusCodes[$httpCode])) {
$message .= ' - ' . $statusCodes[$httpCode] . ' (HTTP ' . $httpCode . ')';
} else {
$message .= ' - HTTP status code: ' . (string)$httpCode;
}

$curlErrorMessage = curl_error($ch);
if (!empty($curlErrorMessage)) {
$message .= ' - curl error message: ' . $curlErrorMessage;
}
fclose($fp);
unlink($toLocation);
$this->silentLog('[warn] fail to download archive from ' . $fromUrl . '. Error: ' . $httpCode . ' ' . curl_error($ch));
curl_close($ch);

$message .= ' - URL: ' . htmlentities($response['url']);

throw new \Exception($message);
} else {
// download succeeded
$info = curl_getinfo($ch);
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');
return false;
}
// download succeeded
$info = curl_getinfo($ch);
$this->silentLog('[info] download stats: size=' . $this->formatBytes((int)$info['size_download']) . ' bytes; total_time=' . round($info['total_time'], 2) . ' secs; avg speed=' . $this->formatBytes((int)$info['speed_download']) . '/sec');

curl_close($ch);
fclose($fp);

$this->silentLog('[info] end of downloadUpdate()');
return true;
}

/**
* Check if PHP is able to decompress archive format
*/
private function isAbleToDecompress(string $ext): bool {
// Only zip is supported for now
return $ext === 'zip' && extension_loaded($ext);
}

private function downloadProgressCallback(\CurlHandle $resource, int $download_size, int $downloaded, int $upload_size, int $uploaded): void {
Expand All @@ -677,7 +714,7 @@ private function downloadProgressCallback(\CurlHandle $resource, int $download_s
}

private function formatBytes(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];

$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
Expand All @@ -699,13 +736,14 @@ private function getDownloadedFilePath(): string {

$filesInStorageLocation = scandir($storageLocation);
$files = array_values(array_filter($filesInStorageLocation, function (string $path) {
return $path !== '.' && $path !== '..';
// Match files with - in the name and extension (*-*.*)
return preg_match('/^.*-.*\..*$/i', $path);
}));
// only the downloaded archive
if (count($files) !== 1) {
throw new \Exception('There are more files than the downloaded archive in the downloads/ folder.');
}
return $storageLocation . '/' . $files[0];
return $storageLocation . $files[0];
}

/**
Expand Down
Loading