mirror of
https://github.com/nextcloud/server.git
synced 2025-01-16 08:09:00 +00:00
9836e9b164
Signed-off-by: provokateurin <kate@provokateurin.de>
240 lines
8 KiB
PHP
240 lines
8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
namespace OC\Lock;
|
|
|
|
use OC\DB\QueryBuilder\Literal;
|
|
use OCP\AppFramework\Utility\ITimeFactory;
|
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
|
use OCP\IDBConnection;
|
|
use OCP\Lock\ILockingProvider;
|
|
use OCP\Lock\LockedException;
|
|
|
|
/**
|
|
* Locking provider that stores the locks in the database
|
|
*/
|
|
class DBLockingProvider extends AbstractLockingProvider {
|
|
private array $sharedLocks = [];
|
|
|
|
public function __construct(
|
|
private IDBConnection $connection,
|
|
private ITimeFactory $timeFactory,
|
|
int $ttl = 3600,
|
|
private bool $cacheSharedLocks = true,
|
|
) {
|
|
parent::__construct($ttl);
|
|
}
|
|
|
|
/**
|
|
* Check if we have an open shared lock for a path
|
|
*/
|
|
protected function isLocallyLocked(string $path): bool {
|
|
return isset($this->sharedLocks[$path]) && $this->sharedLocks[$path];
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
protected function markAcquire(string $path, int $targetType): void {
|
|
parent::markAcquire($path, $targetType);
|
|
if ($this->cacheSharedLocks) {
|
|
if ($targetType === self::LOCK_SHARED) {
|
|
$this->sharedLocks[$path] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the type of an existing tracked lock
|
|
*/
|
|
protected function markChange(string $path, int $targetType): void {
|
|
parent::markChange($path, $targetType);
|
|
if ($this->cacheSharedLocks) {
|
|
if ($targetType === self::LOCK_SHARED) {
|
|
$this->sharedLocks[$path] = true;
|
|
} elseif ($targetType === self::LOCK_EXCLUSIVE) {
|
|
$this->sharedLocks[$path] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a file locking row if it does not exists.
|
|
*/
|
|
protected function initLockField(string $path, int $lock = 0): int {
|
|
$expire = $this->getExpireTime();
|
|
return $this->connection->insertIgnoreConflict('file_locks', [
|
|
'key' => $path,
|
|
'lock' => $lock,
|
|
'ttl' => $expire
|
|
]);
|
|
}
|
|
|
|
protected function getExpireTime(): int {
|
|
return $this->timeFactory->getTime() + $this->ttl;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function isLocked(string $path, int $type): bool {
|
|
if ($this->hasAcquiredLock($path, $type)) {
|
|
return true;
|
|
}
|
|
$query = $this->connection->getQueryBuilder();
|
|
$query->select('lock')
|
|
->from('file_locks')
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path)));
|
|
$result = $query->executeQuery();
|
|
$lockValue = (int)$result->fetchOne();
|
|
if ($type === self::LOCK_SHARED) {
|
|
if ($this->isLocallyLocked($path)) {
|
|
// if we have a shared lock we kept open locally but it's released we always have at least 1 shared lock in the db
|
|
return $lockValue > 1;
|
|
} else {
|
|
return $lockValue > 0;
|
|
}
|
|
} elseif ($type === self::LOCK_EXCLUSIVE) {
|
|
return $lockValue === -1;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function acquireLock(string $path, int $type, ?string $readablePath = null): void {
|
|
$expire = $this->getExpireTime();
|
|
if ($type === self::LOCK_SHARED) {
|
|
if (!$this->isLocallyLocked($path)) {
|
|
$result = $this->initLockField($path, 1);
|
|
if ($result <= 0) {
|
|
$query = $this->connection->getQueryBuilder();
|
|
$query->update('file_locks')
|
|
->set('lock', $query->func()->add('lock', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
|
|
->set('ttl', $query->createNamedParameter($expire))
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path)))
|
|
->andWhere($query->expr()->gte('lock', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
|
$result = $query->executeStatement();
|
|
}
|
|
} else {
|
|
$result = 1;
|
|
}
|
|
} else {
|
|
$existing = 0;
|
|
if ($this->hasAcquiredLock($path, ILockingProvider::LOCK_SHARED) === false && $this->isLocallyLocked($path)) {
|
|
$existing = 1;
|
|
}
|
|
$result = $this->initLockField($path, -1);
|
|
if ($result <= 0) {
|
|
$query = $this->connection->getQueryBuilder();
|
|
$query->update('file_locks')
|
|
->set('lock', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
|
|
->set('ttl', $query->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
|
|
->where($query->expr()->eq('key', $query->createNamedParameter($path)))
|
|
->andWhere($query->expr()->eq('lock', $query->createNamedParameter($existing)));
|
|
$result = $query->executeStatement();
|
|
}
|
|
}
|
|
if ($result !== 1) {
|
|
throw new LockedException($path, null, null, $readablePath);
|
|
}
|
|
$this->markAcquire($path, $type);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function releaseLock(string $path, int $type): void {
|
|
$this->markRelease($path, $type);
|
|
|
|
// we keep shared locks till the end of the request so we can re-use them
|
|
if ($type === self::LOCK_EXCLUSIVE) {
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$qb->update('file_locks')
|
|
->set('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))
|
|
->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
|
|
->andWhere($qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
|
|
$qb->executeStatement();
|
|
} elseif (!$this->cacheSharedLocks) {
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$qb->update('file_locks')
|
|
->set('lock', $qb->func()->subtract('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)))
|
|
->where($qb->expr()->eq('key', $qb->createNamedParameter($path)))
|
|
->andWhere($qb->expr()->gt('lock', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
|
$qb->executeStatement();
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function changeLock(string $path, int $targetType): void {
|
|
$expire = $this->getExpireTime();
|
|
if ($targetType === self::LOCK_SHARED) {
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$result = $qb->update('file_locks')
|
|
->set('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
|
|
->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
|
|
->where($qb->expr()->andX(
|
|
$qb->expr()->eq('key', $qb->createNamedParameter($path)),
|
|
$qb->expr()->eq('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
|
|
))->executeStatement();
|
|
} else {
|
|
// since we only keep one shared lock in the db we need to check if we have more then one shared lock locally manually
|
|
if (isset($this->acquiredLocks['shared'][$path]) && $this->acquiredLocks['shared'][$path] > 1) {
|
|
throw new LockedException($path);
|
|
}
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$result = $qb->update('file_locks')
|
|
->set('lock', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
|
|
->set('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT))
|
|
->where($qb->expr()->andX(
|
|
$qb->expr()->eq('key', $qb->createNamedParameter($path)),
|
|
$qb->expr()->eq('lock', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
|
|
))->executeStatement();
|
|
}
|
|
if ($result !== 1) {
|
|
throw new LockedException($path);
|
|
}
|
|
$this->markChange($path, $targetType);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function cleanExpiredLocks(): void {
|
|
$expire = $this->timeFactory->getTime();
|
|
try {
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$qb->delete('file_locks')
|
|
->where($qb->expr()->lt('ttl', $qb->createNamedParameter($expire, IQueryBuilder::PARAM_INT)))
|
|
->executeStatement();
|
|
} catch (\Exception $e) {
|
|
// If the table is missing, the clean up was successful
|
|
if ($this->connection->tableExists('file_locks')) {
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function releaseAll(): void {
|
|
parent::releaseAll();
|
|
|
|
if (!$this->cacheSharedLocks) {
|
|
return;
|
|
}
|
|
// since we keep shared locks we need to manually clean those
|
|
$lockedPaths = array_keys($this->sharedLocks);
|
|
$lockedPaths = array_filter($lockedPaths, function ($path) {
|
|
return $this->sharedLocks[$path];
|
|
});
|
|
|
|
$chunkedPaths = array_chunk($lockedPaths, 100);
|
|
|
|
$qb = $this->connection->getQueryBuilder();
|
|
$qb->update('file_locks')
|
|
->set('lock', $qb->func()->subtract('lock', $qb->expr()->literal(1)))
|
|
->where($qb->expr()->in('key', $qb->createParameter('chunk')))
|
|
->andWhere($qb->expr()->gt('lock', new Literal(0)));
|
|
|
|
foreach ($chunkedPaths as $chunk) {
|
|
$qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
|
|
$qb->executeStatement();
|
|
}
|
|
}
|
|
}
|