generated from Nodarx/template
136 lines
4.3 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|