Skip to content

Commit c10107b

Browse files
committed
feat(IUserFolder): add a user folder class
* Implements #52896 Similar to the root folder this represents the users home folder, it allows to group user based methods on the folder like the user quota. Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent e5c6ab4 commit c10107b

File tree

7 files changed

+172
-24
lines changed

7 files changed

+172
-24
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@
440440
'OCP\\Files\\IMimeTypeDetector' => $baseDir . '/lib/public/Files/IMimeTypeDetector.php',
441441
'OCP\\Files\\IMimeTypeLoader' => $baseDir . '/lib/public/Files/IMimeTypeLoader.php',
442442
'OCP\\Files\\IRootFolder' => $baseDir . '/lib/public/Files/IRootFolder.php',
443+
'OCP\\Files\\IUserFolder' => $baseDir . '/lib/public/Files/IUserFolder.php',
443444
'OCP\\Files\\InvalidCharacterInPathException' => $baseDir . '/lib/public/Files/InvalidCharacterInPathException.php',
444445
'OCP\\Files\\InvalidContentException' => $baseDir . '/lib/public/Files/InvalidContentException.php',
445446
'OCP\\Files\\InvalidDirectoryException' => $baseDir . '/lib/public/Files/InvalidDirectoryException.php',
@@ -1635,6 +1636,7 @@
16351636
'OC\\Files\\Node\\NonExistingFile' => $baseDir . '/lib/private/Files/Node/NonExistingFile.php',
16361637
'OC\\Files\\Node\\NonExistingFolder' => $baseDir . '/lib/private/Files/Node/NonExistingFolder.php',
16371638
'OC\\Files\\Node\\Root' => $baseDir . '/lib/private/Files/Node/Root.php',
1639+
'OC\\Files\\Node\\UserFolder' => $baseDir . '/lib/private/Files/Node/UserFolder.php',
16381640
'OC\\Files\\Notify\\Change' => $baseDir . '/lib/private/Files/Notify/Change.php',
16391641
'OC\\Files\\Notify\\RenameChange' => $baseDir . '/lib/private/Files/Notify/RenameChange.php',
16401642
'OC\\Files\\ObjectStore\\AppdataPreviewObjectStoreStorage' => $baseDir . '/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
481481
'OCP\\Files\\IMimeTypeDetector' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeDetector.php',
482482
'OCP\\Files\\IMimeTypeLoader' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeLoader.php',
483483
'OCP\\Files\\IRootFolder' => __DIR__ . '/../../..' . '/lib/public/Files/IRootFolder.php',
484+
'OCP\\Files\\IUserFolder' => __DIR__ . '/../../..' . '/lib/public/Files/IUserFolder.php',
484485
'OCP\\Files\\InvalidCharacterInPathException' => __DIR__ . '/../../..' . '/lib/public/Files/InvalidCharacterInPathException.php',
485486
'OCP\\Files\\InvalidContentException' => __DIR__ . '/../../..' . '/lib/public/Files/InvalidContentException.php',
486487
'OCP\\Files\\InvalidDirectoryException' => __DIR__ . '/../../..' . '/lib/public/Files/InvalidDirectoryException.php',
@@ -1676,6 +1677,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
16761677
'OC\\Files\\Node\\NonExistingFile' => __DIR__ . '/../../..' . '/lib/private/Files/Node/NonExistingFile.php',
16771678
'OC\\Files\\Node\\NonExistingFolder' => __DIR__ . '/../../..' . '/lib/private/Files/Node/NonExistingFolder.php',
16781679
'OC\\Files\\Node\\Root' => __DIR__ . '/../../..' . '/lib/private/Files/Node/Root.php',
1680+
'OC\\Files\\Node\\UserFolder' => __DIR__ . '/../../..' . '/lib/private/Files/Node/UserFolder.php',
16791681
'OC\\Files\\Notify\\Change' => __DIR__ . '/../../..' . '/lib/private/Files/Notify/Change.php',
16801682
'OC\\Files\\Notify\\RenameChange' => __DIR__ . '/../../..' . '/lib/private/Files/Notify/RenameChange.php',
16811683
'OC\\Files\\ObjectStore\\AppdataPreviewObjectStoreStorage' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/AppdataPreviewObjectStoreStorage.php',

lib/private/Files/Node/LazyUserFolder.php

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
*/
99
namespace OC\Files\Node;
1010

11+
use OC\Files\View;
1112
use OCP\Constants;
12-
use OCP\Files\File;
1313
use OCP\Files\FileInfo;
1414
use OCP\Files\Folder;
1515
use OCP\Files\IRootFolder;
16+
use OCP\Files\IUserFolder;
1617
use OCP\Files\Mount\IMountManager;
17-
use OCP\Files\NotFoundException;
18+
use OCP\ICacheFactory;
19+
use OCP\IConfig;
1820
use OCP\IUser;
21+
use OCP\Server;
1922
use Psr\Log\LoggerInterface;
2023

21-
class LazyUserFolder extends LazyFolder {
24+
class LazyUserFolder extends LazyFolder implements IUserFolder {
2225
private IUser $user;
2326
private string $path;
2427
private IMountManager $mountManager;
@@ -27,23 +30,30 @@ public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager
2730
$this->user = $user;
2831
$this->mountManager = $mountManager;
2932
$this->path = '/' . $user->getUID() . '/files';
30-
parent::__construct($rootFolder, function () use ($user): Folder {
31-
try {
32-
$node = $this->getRootFolder()->get($this->path);
33-
if ($node instanceof File) {
34-
$e = new \RuntimeException();
35-
\OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [
36-
'exception' => $e,
37-
]);
38-
throw $e;
39-
}
40-
return $node;
41-
} catch (NotFoundException $e) {
42-
if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) {
43-
$this->getRootFolder()->newFolder('/' . $user->getUID());
44-
}
45-
return $this->getRootFolder()->newFolder($this->path);
33+
parent::__construct($rootFolder, function () use ($user): UserFolder {
34+
$root = $this->getRootFolder();
35+
if (!$root->nodeExists('/' . $user->getUID())) {
36+
$parent = $root->newFolder('/' . $user->getUID());
37+
} else {
38+
$parent = $root->get('/' . $user->getUID());
4639
}
40+
if (!($parent instanceof Folder)) {
41+
$e = new \RuntimeException();
42+
\OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [
43+
'exception' => $e,
44+
]);
45+
throw $e;
46+
}
47+
$realFolder = $root->newFolder('/' . $user->getUID() . '/files');
48+
return new UserFolder(
49+
$root,
50+
new View($parent->getPath()),
51+
$realFolder->getPath(),
52+
$parent,
53+
Server::get(IConfig::class),
54+
$user,
55+
Server::get(ICacheFactory::class),
56+
);
4757
}, [
4858
'path' => $this->path,
4959
// Sharing user root folder is not allowed
@@ -63,4 +73,8 @@ public function getMountPoint() {
6373
}
6474
return $mountPoint;
6575
}
76+
77+
public function getUserQuota(bool $useCache = true): array {
78+
return $this->__call(__FUNCTION__, func_get_args());
79+
}
6680
}

lib/private/Files/Node/Root.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OCP\Files\NotPermittedException;
2727
use OCP\ICache;
2828
use OCP\ICacheFactory;
29+
use OCP\IConfig;
2930
use OCP\IUser;
3031
use OCP\IUserManager;
3132
use OCP\Server;
@@ -74,7 +75,7 @@ public function __construct(
7475
LoggerInterface $logger,
7576
IUserManager $userManager,
7677
IEventDispatcher $eventDispatcher,
77-
ICacheFactory $cacheFactory,
78+
private ICacheFactory $cacheFactory,
7879
) {
7980
parent::__construct($this, $view, '');
8081
$this->mountManager = $manager;
@@ -329,7 +330,7 @@ public function getName() {
329330
* Returns a view to user's files folder
330331
*
331332
* @param string $userId user ID
332-
* @return \OCP\Files\Folder
333+
* @return \OCP\Files\IUserFolder
333334
* @throws NoUserException
334335
* @throws NotPermittedException
335336
*/
@@ -362,9 +363,23 @@ public function getUserFolder($userId) {
362363
}
363364
} catch (NotFoundException $e) {
364365
if (!$this->nodeExists('/' . $userId)) {
365-
$this->newFolder('/' . $userId);
366+
$parent = $this->newFolder('/' . $userId);
367+
} else {
368+
$parent = $this->get('/' . $userId);
369+
if (!$parent instanceof \OCP\Files\Folder) {
370+
throw new \Exception("Account folder for \"$userId\" exists as a file");
371+
}
366372
}
367-
$folder = $this->newFolder('/' . $userId . '/files');
373+
$realFolder = $this->newFolder('/' . $userId . '/files');
374+
$folder = new UserFolder(
375+
$this->root,
376+
$this->view,
377+
$realFolder->getPath(),
378+
$parent,
379+
Server::get(IConfig::class),
380+
$userObject,
381+
$this->cacheFactory,
382+
);
368383
}
369384
} else {
370385
$folder = new LazyUserFolder($this, $userObject, $this->mountManager);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OC\Files\Node;
9+
10+
use OC\Files\Storage\Wrapper\Quota;
11+
use OC\Files\View;
12+
use OCP\Files\IRootFolder;
13+
use OCP\Files\IUserFolder;
14+
use OCP\ICacheFactory;
15+
use OCP\IConfig;
16+
use OCP\IUser;
17+
18+
/**
19+
* @since 32.0.0
20+
*/
21+
class UserFolder extends Folder implements IUserFolder {
22+
23+
public function __construct(
24+
IRootFolder $root,
25+
View $view,
26+
string $path,
27+
Folder $parent,
28+
protected IConfig $config,
29+
protected IUser $user,
30+
protected ICacheFactory $cacheFactory,
31+
) {
32+
parent::__construct($root, $view, $path, parent: $parent);
33+
}
34+
35+
public function getUserQuota(bool $useCache = true): array {
36+
// return from cache if requested and we already cached it
37+
$memcache = $this->cacheFactory->createLocal('storage_info');
38+
if ($useCache) {
39+
$cached = $memcache->get($this->getPath());
40+
if ($cached) {
41+
return $cached;
42+
}
43+
}
44+
45+
$quotaIncludeExternalStorage = $this->config->getSystemValueBool('quota_include_external_storage');
46+
$rootInfo = $this->getFileInfo($quotaIncludeExternalStorage);
47+
48+
/** @var int|float $used */
49+
$used = max($rootInfo->getSize(), 0.0);
50+
/** @var int|float $quota */
51+
$quota = \OCP\Files\FileInfo::SPACE_UNLIMITED;
52+
$mount = $rootInfo->getMountPoint();
53+
$storage = $mount->getStorage();
54+
if ($storage === null) {
55+
throw new \RuntimeException('Storage returned from mount point is null.');
56+
}
57+
58+
if ($storage->instanceOfStorage(Quota::class)) {
59+
/** @var Quota $sourceStorage */
60+
$quota = $sourceStorage->getQuota();
61+
} elseif ($quotaIncludeExternalStorage) {
62+
$quota = $this->user->getQuotaBytes();
63+
}
64+
65+
$free = $storage->free_space($rootInfo->getInternalPath());
66+
if (is_bool($free)) {
67+
$free = 0.0;
68+
}
69+
70+
if ($free >= 0) {
71+
$total = $free + $used;
72+
} else {
73+
$total = $free; //either unknown or unlimited
74+
}
75+
76+
$relative = $total > 0
77+
? $used / $total
78+
: 0;
79+
$this->config->setUserValue($this->user->getUID(), 'files', 'lastSeenQuotaUsage', (string)$relative);
80+
81+
$info = [
82+
'free' => $free,
83+
'used' => $used,
84+
'quota' => $quota,
85+
'total' => $total,
86+
];
87+
$memcache->set($this->getPath(), $info, 5 * 60);
88+
89+
return $info;
90+
}
91+
92+
}

lib/public/Files/IRootFolder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ interface IRootFolder extends Folder, Emitter {
2323
* Returns a view to user's files folder
2424
*
2525
* @param string $userId user ID
26-
* @return Folder
26+
* @return IUserFolder
2727
* @throws NoUserException
2828
* @throws NotPermittedException
2929
*
3030
* @since 8.2.0
31+
* @since 32.0.0 returns OCP\Files\IUserFolder instead of OCP\Files\Folder
3132
*/
3233
public function getUserFolder($userId);
3334

lib/public/Files/IUserFolder.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCP\Files;
9+
10+
/**
11+
* @since 32.0.0
12+
*/
13+
interface IUserFolder extends Folder {
14+
15+
/**
16+
* @param bool $useCache - Use the cached value if available instead of recalculate.
17+
* @return array{used: int|float, free: int|float, total: int|float, quota: int|float}
18+
* @since 32.0.0
19+
*/
20+
public function getUserQuota(bool $useCache = true): array;
21+
22+
}

0 commit comments

Comments
 (0)