Skip to content

Commit b826403

Browse files
feat: add federated file locking
Signed-off-by: Benjamin Frueh <[email protected]> fix: use ocp methods instead of sql queries and add path param for folder support Signed-off-by: Benjamin Frueh <[email protected]> feat: request federated locks using webdav propfind Signed-off-by: Benjamin Frueh <[email protected]> Update lib/Service/LockService.php Co-authored-by: Julius Knorr <[email protected]> Signed-off-by: Benjamin Früh <[email protected]> refactor: rename propfind event and use first userfolder node directly Signed-off-by: Benjamin Frueh <[email protected]> Apply suggestions from code review Co-authored-by: Julius Knorr <[email protected]> Signed-off-by: Benjamin Früh <[email protected]>
1 parent 5f94b40 commit b826403

File tree

5 files changed

+120
-21
lines changed

5 files changed

+120
-21
lines changed

lib/AppInfo/Application.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1414
use OCA\FilesLock\Capability;
1515
use OCA\FilesLock\Listeners\LoadAdditionalScripts;
16+
use OCA\FilesLock\Listeners\PropfindPropertiesListener;
1617
use OCA\FilesLock\LockProvider;
1718
use OCA\FilesLock\Service\FileService;
1819
use OCA\FilesLock\Service\LockService;
@@ -21,6 +22,7 @@
2122
use OCP\AppFramework\Bootstrap\IBootContext;
2223
use OCP\AppFramework\Bootstrap\IBootstrap;
2324
use OCP\AppFramework\Bootstrap\IRegistrationContext;
25+
use OCP\Files\Events\BeforeRemotePropfindEvent;
2426
use OCP\Files\Lock\ILockManager;
2527
use OCP\IUserSession;
2628
use OCP\Server;
@@ -48,6 +50,10 @@ public function register(IRegistrationContext $context): void {
4850
LoadAdditionalScriptsEvent::class,
4951
LoadAdditionalScripts::class
5052
);
53+
$context->registerEventListener(
54+
BeforeRemotePropfindEvent::class,
55+
PropfindPropertiesListener::class
56+
);
5157
}
5258

5359
public function boot(IBootContext $context): void {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\FilesLock\Listeners;
11+
12+
use OCA\FilesLock\AppInfo\Application;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Files\Events\BeforeRemotePropfindEvent;
16+
17+
/**
18+
* @template-implements IEventListener<BeforeRemotePropfindEvent>
19+
*/
20+
class PropfindPropertiesListener implements IEventListener {
21+
public function handle(Event $event): void {
22+
if (!($event instanceof BeforeRemotePropfindEvent)) {
23+
return;
24+
}
25+
26+
$event->addProperties([
27+
Application::DAV_PROPERTY_LOCK,
28+
Application::DAV_PROPERTY_LOCK_OWNER,
29+
Application::DAV_PROPERTY_LOCK_OWNER_DISPLAYNAME,
30+
Application::DAV_PROPERTY_LOCK_OWNER_TYPE,
31+
Application::DAV_PROPERTY_LOCK_EDITOR,
32+
Application::DAV_PROPERTY_LOCK_TIME,
33+
Application::DAV_PROPERTY_LOCK_TIMEOUT,
34+
Application::DAV_PROPERTY_LOCK_TOKEN,
35+
]);
36+
}
37+
}

lib/LockProvider.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,13 @@ public function __construct(LockService $lockService) {
2020
}
2121

2222
public function getLocks(int $fileId): array {
23-
try {
24-
$lock = $this->lockService->getLockFromFileId($fileId);
25-
$this->lockService->injectMetadata($lock);
26-
} catch (Exceptions\LockNotFoundException $e) {
23+
$lock = $this->lockService->getLockForNodeId($fileId);
24+
if (!$lock) {
2725
return [];
2826
}
2927

30-
if ($lock) {
31-
return [$lock];
32-
}
33-
return [];
28+
$this->lockService->injectMetadata($lock);
29+
return [$lock];
3430
}
3531

3632
/**

lib/Service/LockService.php

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
namespace OCA\FilesLock\Service;
1111

1212
use Exception;
13+
use OC\Files\Storage\DAV;
14+
use OC\Files\Storage\Wrapper\Wrapper;
15+
use OCA\FilesLock\AppInfo\Application;
1316
use OCA\FilesLock\Db\LocksRequest;
1417
use OCA\FilesLock\Exceptions\LockNotFoundException;
1518
use OCA\FilesLock\Exceptions\UnauthorizedUnlockException;
@@ -19,6 +22,7 @@
1922
use OCP\Constants;
2023
use OCP\EventDispatcher\IEventDispatcher;
2124
use OCP\Files\InvalidPathException;
25+
use OCP\Files\IRootFolder;
2226
use OCP\Files\Lock\ILock;
2327
use OCP\Files\Lock\LockContext;
2428
use OCP\Files\Lock\OwnerLockedException;
@@ -65,6 +69,7 @@ public function __construct(
6569
IUserSession $userSession,
6670
IRequest $request,
6771
LoggerInterface $logger,
72+
private IRootFolder $rootFolder,
6873
) {
6974
$this->l10n = $l10n;
7075
$this->userManager = $userManager;
@@ -90,8 +95,9 @@ public function getLockForNodeId(int $nodeId) {
9095

9196
try {
9297
$this->lockCache[$nodeId] = $this->getLockFromFileId($nodeId);
93-
} catch (LockNotFoundException $e) {
94-
$this->lockCache[$nodeId] = false;
98+
} catch (LockNotFoundException) {
99+
$remoteLock = $this->getRemoteLockFromDav($nodeId);
100+
$this->lockCache[$nodeId] = $remoteLock ?: false;
95101
}
96102

97103
return $this->lockCache[$nodeId];
@@ -110,7 +116,6 @@ public function getLockForNodeIds(array $nodeIds): array {
110116
$locks[$nodeId] = $this->lockCache[$nodeId];
111117
} else {
112118
$locksToRequest[] = $nodeId;
113-
$this->lockCache[$nodeId] = false;
114119
}
115120
}
116121
if (count($locksToRequest) === 0) {
@@ -394,6 +399,61 @@ function (FileLock $lock) {
394399
$this->locksRequest->removeIds($ids);
395400
}
396401

402+
public function getRemoteLockFromDav(int $nodeId): ?FileLock {
403+
try {
404+
$user = $this->userSession->getUser();
405+
if (!$user) {
406+
return null;
407+
}
408+
409+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
410+
$node = $userFolder->getFirstNodeById($nodeId);
411+
if (empty($node)) {
412+
return null;
413+
}
414+
415+
$storage = $node->getStorage();
416+
417+
while ($storage->instanceOfStorage(Wrapper::class)) {
418+
$storage = $storage->getWrapperStorage();
419+
}
420+
421+
if (!$storage->instanceOfStorage(DAV::class)) {
422+
return null;
423+
}
424+
425+
$path = $node->getInternalPath();
426+
$storage->getMetaData($path);
427+
428+
if (!method_exists($storage, 'getPropfindPropertyValue')) {
429+
return null;
430+
}
431+
432+
$isLocked = $storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK);
433+
if (!$isLocked) {
434+
return null;
435+
}
436+
437+
$fileLock = new FileLock();
438+
$fileLock->import([
439+
'fileId' => $nodeId,
440+
'owner' => (string)($storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK_OWNER_DISPLAYNAME) ?? ''),
441+
'type' => (int)($storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK_OWNER_TYPE) ?? 0),
442+
'creation' => (int)($storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK_TIME) ?? 0),
443+
'ttl' => (int)($storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK_TIMEOUT) ?? 0),
444+
'token' => (string)($storage->getPropfindPropertyValue($path, Application::DAV_PROPERTY_LOCK_TOKEN) ?? ''),
445+
]);
446+
447+
$remoteHost = parse_url($storage->getRemote(), PHP_URL_HOST);
448+
$fileLock->setDisplayName($fileLock->getDisplayName() . '@' . $remoteHost);
449+
450+
return $fileLock;
451+
} catch (\Exception $e) {
452+
$this->logger->error('Failed to get remote lock from DAV: ' . $e->getMessage(), ['exception' => $e]);
453+
return null;
454+
}
455+
}
456+
397457
private function propagateEtag(LockContext $lockContext): void {
398458
$node = $lockContext->getNode();
399459
$node->getStorage()->getCache()->update($node->getId(), [

lib/Storage/LockWrapper.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function rename($path1, $path2): bool {
134134
//This is a rename of the transfer file to the original file
135135
if (strpos($part, '.ocTransferId') === 0) {
136136
return $this->checkPermissions($path2, Constants::PERMISSION_CREATE)
137-
&& parent::rename($path1, $path2);
137+
&& parent::rename($path1, $path2);
138138
}
139139
}
140140
$permissions
@@ -145,20 +145,20 @@ public function rename($path1, $path2): bool {
145145
}
146146

147147
return $this->checkPermissions($sourceParent, Constants::PERMISSION_DELETE)
148-
&& $this->checkPermissions($path1, Constants::PERMISSION_UPDATE & Constants::PERMISSION_READ)
149-
&& $this->checkPermissions($path2, $permissions)
150-
&& parent::rename($path1, $path2);
148+
&& $this->checkPermissions($path1, Constants::PERMISSION_UPDATE & Constants::PERMISSION_READ)
149+
&& $this->checkPermissions($path2, $permissions)
150+
&& parent::rename($path1, $path2);
151151
}
152152

153153
public function copy($path1, $path2): bool {
154154
$permissions
155155
= $this->file_exists($path2) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
156156

157157
return $this->checkPermissions($path2, $permissions)
158-
&& $this->checkPermissions(
159-
$path1, Constants::PERMISSION_READ
160-
)
161-
&& parent::copy($path1, $path2);
158+
&& $this->checkPermissions(
159+
$path1, Constants::PERMISSION_READ
160+
)
161+
&& parent::copy($path1, $path2);
162162
}
163163

164164
public function touch($path, $mtime = null): bool {
@@ -174,12 +174,12 @@ public function mkdir($path): bool {
174174

175175
public function rmdir($path): bool {
176176
return $this->checkPermissions($path, Constants::PERMISSION_DELETE)
177-
&& parent::rmdir($path);
177+
&& parent::rmdir($path);
178178
}
179179

180180
public function unlink($path): bool {
181181
return $this->checkPermissions($path, Constants::PERMISSION_DELETE)
182-
&& parent::unlink($path);
182+
&& parent::unlink($path);
183183
}
184184

185185
public function file_put_contents($path, $data): int|float|false {

0 commit comments

Comments
 (0)