Raiju/includes/json-store.php
2026-10-25 06:05:42 +03:00

62 lines
2.2 KiB
PHP

<?php
/** Atomic JSON writes and transactions for shared flat-file records. */
function raijuJsonLock(string $path) {
$dir = dirname($path);
if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) return false;
$handle = @fopen($path . '.lock', 'c');
if ($handle === false) return false;
if (!flock($handle, LOCK_EX)) {
fclose($handle);
return false;
}
return $handle;
}
function raijuJsonAtomicWrite(string $path, array $data): bool {
try {
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return false;
}
$dir = dirname($path);
if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) return false;
$temporary = @tempnam($dir, '.raiju-json-');
if ($temporary === false) return false;
try {
if (file_put_contents($temporary, $json) !== strlen($json)) return false;
@chmod($temporary, is_file($path) ? (fileperms($path) & 0777) : (0666 & ~umask()));
return rename($temporary, $path);
} finally {
if (is_file($temporary)) @unlink($temporary);
}
}
/** The callback edits the array by reference; false aborts without writing. */
function raijuJsonUpdate(string $path, callable $update, array $default = []): bool {
$lock = raijuJsonLock($path);
if ($lock === false) return false;
try {
$data = $default;
if (is_file($path)) {
try {
$data = json_decode((string)file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return false; // Never replace a damaged store with empty data.
}
if (!is_array($data)) return false;
}
if ($update($data) === false) return false;
return raijuJsonAtomicWrite($path, $data);
} finally {
flock($lock, LOCK_UN);
fclose($lock);
// Keep the lock inode stable for other processes already waiting.
}
}
/** Revision of the stored bytes, including an explicit missing-file version. */
function raijuJsonRevision(string $path): string {
return is_file($path) ? (hash_file('sha256', $path) ?: 'unreadable') : 'missing';
}