TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
7.3 KiB
234 lines
7.3 KiB
<?php
|
|
|
|
/**
|
|
* JSON-backed CRUD repository. Writes use a temporary file and rename so a
|
|
* crash cannot leave a partially written configuration.
|
|
*/
|
|
class TunnelRepository
|
|
{
|
|
private string $file;
|
|
|
|
public function __construct(string $file)
|
|
{
|
|
$this->file = $file;
|
|
}
|
|
|
|
/** @return array<TunnelRule> */
|
|
public function all(): array
|
|
{
|
|
$primaryError = null;
|
|
if (is_file($this->file)) {
|
|
try {
|
|
return $this->readRules($this->file);
|
|
} catch (Throwable $error) {
|
|
$primaryError = $error;
|
|
}
|
|
}
|
|
|
|
$backup = $this->backupFile();
|
|
if (is_file($backup)) {
|
|
try {
|
|
$rules = $this->readRules($backup);
|
|
$json = file_get_contents($backup);
|
|
if ($json === false) {
|
|
throw new RuntimeException('无法读取备份配置:' . $backup);
|
|
}
|
|
$this->ensureDirectory();
|
|
$temporary = $this->file . '.recover.tmp';
|
|
$this->writeDurably($temporary, $json);
|
|
$this->replaceFile($temporary, $this->file);
|
|
return $rules;
|
|
} catch (Throwable $backupError) {
|
|
if ($primaryError !== null) {
|
|
throw new RuntimeException(
|
|
'主配置和备份配置均已损坏:'
|
|
. $primaryError->getMessage() . ';'
|
|
. $backupError->getMessage()
|
|
);
|
|
}
|
|
throw $backupError;
|
|
}
|
|
}
|
|
|
|
if ($primaryError !== null) {
|
|
throw $primaryError;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/** @return array<TunnelRule> */
|
|
private function readRules(string $file): array
|
|
{
|
|
$json = file_get_contents($file);
|
|
if ($json === false || trim($json) === '') {
|
|
throw new RuntimeException('配置文件为空或无法读取:' . $file);
|
|
}
|
|
$rows = json_decode($json, true);
|
|
if (!is_array($rows)) {
|
|
throw new RuntimeException('配置文件不是有效的 JSON:' . $file);
|
|
}
|
|
|
|
$rules = [];
|
|
foreach ($rows as $row) {
|
|
if (is_array($row)) {
|
|
$rules[] = new TunnelRule($row);
|
|
}
|
|
}
|
|
return $rules;
|
|
}
|
|
|
|
public function find(string $id): ?TunnelRule
|
|
{
|
|
foreach ($this->all() as $rule) {
|
|
if ($rule->id === $id) {
|
|
return $rule;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function create(TunnelRule $rule): void
|
|
{
|
|
$rules = $this->all();
|
|
foreach ($rules as $current) {
|
|
if ($current->id === $rule->id) {
|
|
throw new RuntimeException('规则 ID 已存在:' . $rule->id);
|
|
}
|
|
}
|
|
$rules[] = $rule;
|
|
$this->save($rules);
|
|
}
|
|
|
|
public function update(TunnelRule $rule): void
|
|
{
|
|
$rules = $this->all();
|
|
$found = false;
|
|
foreach ($rules as $index => $current) {
|
|
if ($current->id === $rule->id) {
|
|
$rules[$index] = $rule;
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
throw new RuntimeException('规则不存在:' . $rule->id);
|
|
}
|
|
$this->save($rules);
|
|
}
|
|
|
|
public function delete(string $id): void
|
|
{
|
|
$rules = [];
|
|
$found = false;
|
|
foreach ($this->all() as $rule) {
|
|
if ($rule->id === $id) {
|
|
$found = true;
|
|
} else {
|
|
$rules[] = $rule;
|
|
}
|
|
}
|
|
if (!$found) {
|
|
throw new RuntimeException('规则不存在:' . $id);
|
|
}
|
|
$this->save($rules);
|
|
}
|
|
|
|
/** @param array<TunnelRule> $rules */
|
|
private function save(array $rules): void
|
|
{
|
|
$this->ensureDirectory();
|
|
|
|
$rows = [];
|
|
foreach ($rules as $rule) {
|
|
$rows[] = $rule->toArray();
|
|
}
|
|
$json = json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
if ($json === false) {
|
|
throw new RuntimeException('无法序列化隧道配置');
|
|
}
|
|
$json .= "\n";
|
|
|
|
// Keep a valid fallback before replacing the primary file. After the
|
|
// primary replacement succeeds, mirror the new version to the backup
|
|
// as well. At every interruption point at least one complete copy
|
|
// remains available.
|
|
$backupJson = $json;
|
|
if (is_file($this->file)) {
|
|
try {
|
|
$this->readRules($this->file);
|
|
$current = file_get_contents($this->file);
|
|
if ($current !== false) {
|
|
$backupJson = $current;
|
|
}
|
|
} catch (Throwable) {
|
|
// Never copy a corrupt primary over the valid backup.
|
|
}
|
|
}
|
|
|
|
$backupTemporary = $this->backupFile() . '.tmp';
|
|
$this->writeDurably($backupTemporary, $backupJson);
|
|
$this->replaceFile($backupTemporary, $this->backupFile());
|
|
|
|
$temporary = $this->file . '.tmp';
|
|
$this->writeDurably($temporary, $json);
|
|
$this->replaceFile($temporary, $this->file);
|
|
|
|
$this->writeDurably($backupTemporary, $json);
|
|
$this->replaceFile($backupTemporary, $this->backupFile());
|
|
}
|
|
|
|
private function ensureDirectory(): void
|
|
{
|
|
$directory = dirname($this->file);
|
|
if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
|
|
throw new RuntimeException('无法创建配置目录:' . $directory);
|
|
}
|
|
}
|
|
|
|
private function backupFile(): string
|
|
{
|
|
return $this->file . '.bak';
|
|
}
|
|
|
|
private function writeDurably(string $file, string $content): void
|
|
{
|
|
$handle = fopen($file, 'wb');
|
|
if ($handle === false) {
|
|
throw new RuntimeException('无法打开配置文件:' . $file);
|
|
}
|
|
|
|
$length = strlen($content);
|
|
$offset = 0;
|
|
while ($offset < $length) {
|
|
$written = fwrite($handle, substr($content, $offset));
|
|
if ($written === false || $written === 0) {
|
|
fclose($handle);
|
|
throw new RuntimeException('无法完整写入配置:' . $file);
|
|
}
|
|
$offset += $written;
|
|
}
|
|
if (!fflush($handle)) {
|
|
fclose($handle);
|
|
throw new RuntimeException('无法刷新配置:' . $file);
|
|
}
|
|
if (function_exists('fsync') && !fsync($handle)) {
|
|
fclose($handle);
|
|
throw new RuntimeException('无法同步配置到磁盘:' . $file);
|
|
}
|
|
fclose($handle);
|
|
chmod($file, 0600);
|
|
}
|
|
|
|
private function replaceFile(string $source, string $destination): void
|
|
{
|
|
// PHP's Windows rename cannot consistently replace an existing file
|
|
// across all supported runtimes. The durable backup written above
|
|
// makes this fallback recoverable.
|
|
if (PHP_OS_FAMILY === 'Windows' && is_file($destination) && !unlink($destination)) {
|
|
throw new RuntimeException('无法替换已有配置:' . $destination);
|
|
}
|
|
if (!rename($source, $destination)) {
|
|
throw new RuntimeException('无法替换配置:' . $destination);
|
|
}
|
|
}
|
|
}
|
|
|