47 lines
1.7 KiB
PHP
47 lines
1.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MeshVault\Indexer;
|
|
|
|
use Closure;
|
|
use InvalidArgumentException;
|
|
use Throwable;
|
|
|
|
/** Advisory preflight only: free space may change immediately after checking. */
|
|
final readonly class SpaceChecker
|
|
{
|
|
private Closure $freeSpace;
|
|
|
|
/** Margin is a fraction of payload bytes, not a fraction of disk capacity. */
|
|
public function __construct(private float $margin = 0.05, ?callable $freeSpace = null)
|
|
{
|
|
if (!is_finite($margin) || $margin < 0) {
|
|
throw new InvalidArgumentException('Space margin must be finite and nonnegative.');
|
|
}
|
|
$this->freeSpace = $freeSpace === null
|
|
? static fn (string $path): float|false => @disk_free_space($path)
|
|
: Closure::fromCallable($freeSpace);
|
|
}
|
|
|
|
/** Require ceil(bytes * (1 + margin)) available bytes on an existing destination directory. */
|
|
public function check(string $directory, int $bytes): void
|
|
{
|
|
if ($bytes < 0) {
|
|
throw new InvalidArgumentException('Payload size must be nonnegative.');
|
|
}
|
|
try {
|
|
$free = ($this->freeSpace)($directory);
|
|
} catch (Throwable $error) {
|
|
throw new ImportException('SPACE_CHECK_FAILED', 'Cannot query destination free space.', $error);
|
|
}
|
|
if ((!is_float($free) && !is_int($free)) || !is_finite((float) $free) || $free < 0) {
|
|
throw new ImportException('SPACE_CHECK_FAILED', 'Cannot determine destination free space.');
|
|
}
|
|
$required = ceil($bytes + $bytes * $this->margin);
|
|
if (!is_finite($required) || $free < $required) {
|
|
throw new ImportException('DISK_FULL', 'Insufficient free space including the safety margin.');
|
|
}
|
|
}
|
|
}
|