|
15 | 15 |
|
16 | 16 | use CodeIgniter\Cache\Exceptions\CacheException; |
17 | 17 | use CodeIgniter\I18n\Time; |
| 18 | +use CodeIgniter\Lock\LockStoreInterface; |
18 | 19 | use Config\Cache; |
19 | 20 | use Throwable; |
20 | 21 |
|
|
23 | 24 | * |
24 | 25 | * @see \CodeIgniter\Cache\Handlers\FileHandlerTest |
25 | 26 | */ |
26 | | -class FileHandler extends BaseHandler |
| 27 | +class FileHandler extends BaseHandler implements LockStoreInterface |
27 | 28 | { |
28 | 29 | /** |
29 | 30 | * Maximum key length. |
@@ -155,6 +156,78 @@ public function decrement(string $key, int $offset = 1): bool|int |
155 | 156 | return $this->increment($key, -$offset); |
156 | 157 | } |
157 | 158 |
|
| 159 | + public function acquireLock(string $key, string $owner, int $ttl): bool |
| 160 | + { |
| 161 | + return $this->withLockFile($key, static function ($handle) use ($owner, $ttl): bool { |
| 162 | + $data = self::readLockData($handle); |
| 163 | + $now = Time::now()->getTimestamp(); |
| 164 | + |
| 165 | + if ($data !== null && $data['expires'] > $now) { |
| 166 | + return false; |
| 167 | + } |
| 168 | + |
| 169 | + return self::writeLockData($handle, $owner, $now + $ttl); |
| 170 | + }); |
| 171 | + } |
| 172 | + |
| 173 | + public function releaseLock(string $key, string $owner): bool |
| 174 | + { |
| 175 | + return $this->withLockFile($key, static function ($handle) use ($owner): bool { |
| 176 | + $data = self::readLockData($handle); |
| 177 | + |
| 178 | + if ($data === null || $data['owner'] !== $owner) { |
| 179 | + return false; |
| 180 | + } |
| 181 | + |
| 182 | + return self::clearLockFile($handle); |
| 183 | + }); |
| 184 | + } |
| 185 | + |
| 186 | + public function forceReleaseLock(string $key): bool |
| 187 | + { |
| 188 | + return ! is_file($this->path . static::validateKey($key, $this->prefix)) |
| 189 | + || $this->withLockFile($key, static fn ($handle): bool => self::clearLockFile($handle), false); |
| 190 | + } |
| 191 | + |
| 192 | + public function refreshLock(string $key, string $owner, int $ttl): bool |
| 193 | + { |
| 194 | + return $this->withLockFile($key, static function ($handle) use ($owner, $ttl): bool { |
| 195 | + $data = self::readLockData($handle); |
| 196 | + $now = Time::now()->getTimestamp(); |
| 197 | + |
| 198 | + if ($data === null || $data['owner'] !== $owner || $data['expires'] <= $now) { |
| 199 | + return false; |
| 200 | + } |
| 201 | + |
| 202 | + return self::writeLockData($handle, $owner, $now + $ttl); |
| 203 | + }); |
| 204 | + } |
| 205 | + |
| 206 | + public function getLockOwner(string $key): ?string |
| 207 | + { |
| 208 | + $owner = null; |
| 209 | + |
| 210 | + $this->withLockFile($key, static function ($handle) use (&$owner): bool { |
| 211 | + $data = self::readLockData($handle); |
| 212 | + |
| 213 | + if ($data === null) { |
| 214 | + return true; |
| 215 | + } |
| 216 | + |
| 217 | + if ($data['expires'] <= Time::now()->getTimestamp()) { |
| 218 | + self::clearLockFile($handle); |
| 219 | + |
| 220 | + return true; |
| 221 | + } |
| 222 | + |
| 223 | + $owner = $data['owner']; |
| 224 | + |
| 225 | + return true; |
| 226 | + }, false); |
| 227 | + |
| 228 | + return $owner; |
| 229 | + } |
| 230 | + |
158 | 231 | public function clean(): bool |
159 | 232 | { |
160 | 233 | return delete_files($this->path, false, true); |
@@ -229,4 +302,92 @@ protected function getItem(string $filename): array|false |
229 | 302 |
|
230 | 303 | return $data; |
231 | 304 | } |
| 305 | + |
| 306 | + /** |
| 307 | + * @param callable(resource): bool $callback |
| 308 | + */ |
| 309 | + private function withLockFile(string $key, callable $callback, bool $create = true): bool |
| 310 | + { |
| 311 | + $key = static::validateKey($key, $this->prefix); |
| 312 | + $handle = @fopen($this->path . $key, $create ? 'c+b' : 'r+b'); |
| 313 | + |
| 314 | + if ($handle === false) { |
| 315 | + return false; |
| 316 | + } |
| 317 | + |
| 318 | + try { |
| 319 | + if (! flock($handle, LOCK_EX)) { |
| 320 | + return false; |
| 321 | + } |
| 322 | + |
| 323 | + return $callback($handle); |
| 324 | + } finally { |
| 325 | + flock($handle, LOCK_UN); |
| 326 | + fclose($handle); |
| 327 | + |
| 328 | + if (is_file($this->path . $key)) { |
| 329 | + try { |
| 330 | + chmod($this->path . $key, $this->mode); |
| 331 | + } catch (Throwable $e) { |
| 332 | + log_message('debug', 'Failed to set mode on cache lock file: ' . $e); |
| 333 | + } |
| 334 | + } |
| 335 | + } |
| 336 | + } |
| 337 | + |
| 338 | + /** |
| 339 | + * @param resource $handle |
| 340 | + * |
| 341 | + * @return array{owner: string, expires: int}|null |
| 342 | + */ |
| 343 | + private static function readLockData($handle): ?array |
| 344 | + { |
| 345 | + rewind($handle); |
| 346 | + |
| 347 | + $content = stream_get_contents($handle); |
| 348 | + |
| 349 | + if ($content === false || $content === '') { |
| 350 | + return null; |
| 351 | + } |
| 352 | + |
| 353 | + try { |
| 354 | + $data = unserialize($content); |
| 355 | + } catch (Throwable) { |
| 356 | + return null; |
| 357 | + } |
| 358 | + |
| 359 | + if (! is_array($data) || ! isset($data['owner'], $data['expires']) || ! is_string($data['owner']) || ! is_int($data['expires'])) { |
| 360 | + return null; |
| 361 | + } |
| 362 | + |
| 363 | + return $data; |
| 364 | + } |
| 365 | + |
| 366 | + /** |
| 367 | + * @param resource $handle |
| 368 | + */ |
| 369 | + private static function writeLockData($handle, string $owner, int $expires): bool |
| 370 | + { |
| 371 | + rewind($handle); |
| 372 | + |
| 373 | + if (! ftruncate($handle, 0)) { |
| 374 | + return false; |
| 375 | + } |
| 376 | + |
| 377 | + if (fwrite($handle, serialize(['owner' => $owner, 'expires' => $expires])) === false) { |
| 378 | + return false; |
| 379 | + } |
| 380 | + |
| 381 | + return fflush($handle); |
| 382 | + } |
| 383 | + |
| 384 | + /** |
| 385 | + * @param resource $handle |
| 386 | + */ |
| 387 | + private static function clearLockFile($handle): bool |
| 388 | + { |
| 389 | + rewind($handle); |
| 390 | + |
| 391 | + return ftruncate($handle, 0) && fflush($handle); |
| 392 | + } |
232 | 393 | } |
0 commit comments