Files
service_dav/lib/DavUriMap.php
T
2026-07-14 18:24:31 -04:00

136 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\ServiceDav;
use DI\Attribute\Inject;
/**
* Bidirectional mapping between DAV object URIs (chosen by clients) and entity
* identifiers assigned by the provider stores.
*
* Stored as a pair of JSON files per logical "map" (keyed by a mapId that
* encodes tenant / user / calendar-or-addressbook context). File locking is
* used to guard against concurrent modifications.
*
* This class is intentionally simple: it is a best-effort persistence layer for
* single-server deployments. A database-backed implementation should be
* substituted for high-concurrency or multi-node environments.
*/
class DavUriMap
{
private string $dir;
public function __construct(#[Inject('rootDir')] string $rootDir)
{
$this->dir = $rootDir . '/var/dav';
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
/**
* Look up the provider entity identifier for a given DAV object URI.
*
* @param string $mapId Logical map bucket (e.g. "{tenant}:{user}:cal:{calId}")
* @param string $objectUri DAV leaf URI (e.g. "abc-123.ics")
*/
public function getEntityId(string $mapId, string $objectUri): ?string
{
return $this->read($mapId)['obj_to_ent'][$objectUri] ?? null;
}
/**
* Look up the DAV object URI for a given provider entity identifier.
*/
public function getObjectUri(string $mapId, string $entityId): ?string
{
return $this->read($mapId)['ent_to_obj'][(string) $entityId] ?? null;
}
/**
* Record a mapping from a DAV object URI to a provider entity identifier.
*/
public function set(string $mapId, string $objectUri, string|int $entityId): void
{
$entityId = (string) $entityId;
$this->update($mapId, static function (array $data) use ($objectUri, $entityId): array {
$data['obj_to_ent'][$objectUri] = $entityId;
$data['ent_to_obj'][$entityId] = $objectUri;
return $data;
});
}
/**
* Remove the mapping for a DAV object URI.
*/
public function delete(string $mapId, string $objectUri): void
{
$this->update($mapId, static function (array $data) use ($objectUri): array {
$entityId = $data['obj_to_ent'][$objectUri] ?? null;
if ($entityId !== null) {
unset($data['ent_to_obj'][$entityId]);
}
unset($data['obj_to_ent'][$objectUri]);
return $data;
});
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
private function mapFile(string $mapId): string
{
return $this->dir . '/map_' . md5($mapId) . '.json';
}
private function read(string $mapId): array
{
$file = $this->mapFile($mapId);
if (!file_exists($file)) {
return ['obj_to_ent' => [], 'ent_to_obj' => []];
}
$contents = file_get_contents($file);
if ($contents === false) {
return ['obj_to_ent' => [], 'ent_to_obj' => []];
}
return json_decode($contents, true) ?? ['obj_to_ent' => [], 'ent_to_obj' => []];
}
private function update(string $mapId, callable $updater): void
{
if (!is_dir($this->dir)) {
mkdir($this->dir, 0750, true);
}
$file = $this->mapFile($mapId);
$fh = fopen($file, 'c+');
if ($fh === false) {
return;
}
try {
flock($fh, LOCK_EX);
$contents = stream_get_contents($fh);
$data = json_decode($contents ?: '{}', true)
?? ['obj_to_ent' => [], 'ent_to_obj' => []];
if (!isset($data['obj_to_ent'])) {
$data['obj_to_ent'] = [];
}
if (!isset($data['ent_to_obj'])) {
$data['ent_to_obj'] = [];
}
$data = $updater($data);
ftruncate($fh, 0);
rewind($fh);
fwrite($fh, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
} finally {
flock($fh, LOCK_UN);
fclose($fh);
}
}
}