Skip to content
Draft
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
63 changes: 20 additions & 43 deletions lib/private/MemoryInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,49 @@
declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2018-2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC;

use OCP\Util;

/**
* Helper class that covers memory info.
*/
class MemoryInfo {
public const RECOMMENDED_MEMORY_LIMIT = 512 * 1024 * 1024;

/**
* Tests if the memory limit is greater or equal the recommended value.
* Tests if the memory limit is compliant with the recommendation value.
*
* @return bool
* @throws \InvalidArgumentException (via $this->getMemoryLimit()) if the memory limit is misconfigured.
*/
public function isMemoryLimitSufficient(): bool {
$memoryLimit = $this->getMemoryLimit();
return $memoryLimit === -1 || $memoryLimit >= self::RECOMMENDED_MEMORY_LIMIT;
}

/**
* Returns the php memory limit.
*
* @return int|float The memory limit in bytes.
*/
public function getMemoryLimit(): int|float {
$iniValue = trim(ini_get('memory_limit'));
if ($iniValue === '-1') {
return -1;
} elseif (is_numeric($iniValue)) {
return Util::numericToNumber($iniValue);
} else {
return $this->memoryLimitToBytes($iniValue);
}
}

/**
* Converts the ini memory limit to bytes.
* Returns the interpreted (by PHP) memory limit in bytes.
*
* @param string $memoryLimit The "memory_limit" ini value
* @return int The memory limit in bytes, or -1 if unlimited.
* @throws \InvalidArgumentException if the memory_limit value cannot be parsed.
*/
private function memoryLimitToBytes(string $memoryLimit): int|float {
$last = strtolower(substr($memoryLimit, -1));
$number = substr($memoryLimit, 0, -1);
if (is_numeric($number)) {
$memoryLimit = Util::numericToNumber($number);
} else {
throw new \InvalidArgumentException($number . ' is not a valid numeric string (in memory_limit ini directive)');
public function getMemoryLimit(): int {
$iniValue = ini_get('memory_limit');

set_error_handler(function($errno, $errstr) {
throw new \ErrorException($errstr, 0, $errno);
});

try {
$bytes = ini_parse_quantity($iniValue); // can emit E_WARNING
return $bytes;
} catch (\ErrorException $e) {
throw new \InvalidArgumentException('Error parsing PHP memory_limit ini directive: ' . $e->getMessage());
} finally {
restore_error_handler();
}

// intended fall through
switch ($last) {
case 'g':
$memoryLimit *= 1024;
// no break
case 'm':
$memoryLimit *= 1024;
// no break
case 'k':
$memoryLimit *= 1024;
}

return $memoryLimit;
}
}
91 changes: 71 additions & 20 deletions lib/public/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,24 +525,40 @@ public static function recursiveArraySearch($haystack, $needle, $index = null) {
}

/**
* calculates the maximum upload size respecting system settings, free space and user quota
* Calculates the maximum allowed upload size for a directory,
* considering available free space/quota and PHP upload size configuration.
*
* @param string $dir the current folder where the user currently operates
* @param int|float|null $free the number of bytes free on the storage holding $dir, if not set this will be received from the storage directly
* @return int|float number of bytes representing
* @param string $dir Directory to check free space for.
* @param int|float|null $free Optional: bytes free on storage; retrieved if not provided.
* @return int|float Max upload size in bytes, or -1 if unlimited.
* @since 5.0.0
*/
public static function maxUploadFilesize(string $dir, int|float|null $free = null): int|float {
if (is_null($free) || $free < 0) {
$free = self::freeSpace($dir);
}
return min($free, self::uploadLimit());
$limit = self::uploadLimit();

if ($free === -1 && $limit === -1) {
return -1;
}

// If only one is unlimited, return the finite value
if ($free === -1) {
return $limit;
}
if ($limit === -1) {
return $free;
}
// Both finite
return min($free, $limit);
}

/**
* Calculate free space left within user quota
* @param string $dir the current folder where the user currently operates
* @return int|float number of bytes representing
* Gets the available free space (respecting user quota) for the given directory.
*
* @param string $dir Directory to compute free space for.
* @return int|float Bytes free (0 or more), or INF for unlimited.
* @since 7.0.0
*/
public static function freeSpace(string $dir): int|float {
Expand All @@ -556,22 +572,57 @@ public static function freeSpace(string $dir): int|float {
}

/**
* Calculate PHP upload limit
* Calculates the effective PHP upload limit in bytes, based on ini settings.
*
* @return int|float number of bytes representing
* Returns the strictest limit from `upload_max_filesize` and `post_max_size`,
* treating 0 and -1 as unlimited. Returns -1 if both limits are unlimited.
*
* @return int Effective upload limit in bytes, or -1 if unlimited.
* @throws \InvalidArgumentException If unable to parse the ini values.
* @since 7.0.0
*/
public static function uploadLimit(): int|float {
$ini = Server::get(IniGetWrapper::class);
$upload_max_filesize = self::computerFileSize($ini->get('upload_max_filesize')) ?: 0;
$post_max_size = self::computerFileSize($ini->get('post_max_size')) ?: 0;
if ($upload_max_filesize === 0 && $post_max_size === 0) {
return INF;
} elseif ($upload_max_filesize === 0 || $post_max_size === 0) {
return max($upload_max_filesize, $post_max_size); //only the non 0 value counts
} else {
return min($upload_max_filesize, $post_max_size);
public static function uploadLimit(): int {
$uploadMaxString = ini_get('upload_max_filesize');
$postMaxString = ini_get('post_max_size');

set_error_handler(function($errno, $errstr) {
throw new \ErrorException($errstr, 0, $errno);
});

try {
$uploadMax = ini_parse_quantity($uploadMaxString);
$postMax = ini_parse_quantity($postMaxString);
} catch (\ErrorException $e) {
throw new \InvalidArgumentException(
'Error parsing PHP upload_max_filesize or post_max_size ini directive: ' . $e->getMessage()
);
} finally {
restore_error_handler();
}

// For these config parameters, both -1 and 0 mean unlimited in modern PHP, so normalize 0 to -1.
$uploadMax = ($uploadMax === 0) ? -1 : $uploadMax;
$postMax = ($postMax === 0) ? -1 : $postMax;

if ($uploadMax === -1 && $postMax === -1) {
return -1; // unlimited
}

if ($uploadMax > $postMax && $postMax !== -1) {
// Optional: Log a warning if upload_max_filesize exceeds post_max_size (or a setup check)
// Actual upload limit will be restricted by post_max_size
return $postMax;
}

if ($uploadMax === -1) {
return $postMax;
}
if ($postMax === -1) {
return $uploadMax;
}

// Normal case: return the most restrictive (lowest) finite limit
return min($uploadMax, $postMax);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion tests/lib/MemoryInfoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ public function restoreMemoryInfoIniSetting() {
}

public static function getMemoryLimitTestData(): array {
// On 32-bit, '2G' should overflow; on 64-bit, it'll be 2147483648
$twoG = PHP_INT_SIZE === 4 ? -2147483648 : 2147483648;
return [
'unlimited' => ['-1', -1,],
'524288000 bytes' => ['524288000', 524288000,],
'500M' => ['500M', 524288000,],
'512000K' => ['512000K', 524288000,],
'2G' => ['2G', 2147483648,],
'2G' => ['2G', $twoG,],
];
}

Expand Down
Loading