diff --git a/apps/files/lib/Command/Mount/ListMounts.php b/apps/files/lib/Command/Mount/ListMounts.php
index b4abeac5ab8af..487e769ad2c94 100644
--- a/apps/files/lib/Command/Mount/ListMounts.php
+++ b/apps/files/lib/Command/Mount/ListMounts.php
@@ -8,17 +8,18 @@
namespace OCA\Files\Command\Mount;
+use OC\Core\Command\Base;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountPoint;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class ListMounts extends Command {
+class ListMounts extends Base {
public function __construct(
private readonly IUserManager $userManager,
private readonly IUserMountCache $userMountCache,
@@ -28,52 +29,81 @@ public function __construct(
}
protected function configure(): void {
+ parent::configure();
$this
->setName('files:mount:list')
->setDescription('List of mounts for a user')
- ->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for');
+ ->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for')
+ ->addOption('cached-only', null, InputOption::VALUE_NONE, 'Only return cached mounts, prevents filesystem setup');
}
public function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument('user');
+ $cachedOnly = $input->getOption('cached-only');
$user = $this->userManager->get($userId);
if (!$user) {
$output->writeln("User $userId not found");
return 1;
}
- $mounts = $this->mountProviderCollection->getMountsForUser($user);
- $mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
- /** @var array $cachedByMountpoint */
- $mountsByMountpoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
+ if ($cachedOnly) {
+ $mounts = [];
+ } else {
+ $mounts = $this->mountProviderCollection->getMountsForUser($user);
+ $mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
+ }
+ /** @var array $cachedByMountPoint */
+ $mountsByMountPoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
usort($mounts, fn (IMountPoint $a, IMountPoint $b) => $a->getMountPoint() <=> $b->getMountPoint());
$cachedMounts = $this->userMountCache->getMountsForUser($user);
usort($cachedMounts, fn (ICachedMountInfo $a, ICachedMountInfo $b) => $a->getMountPoint() <=> $b->getMountPoint());
/** @var array $cachedByMountpoint */
- $cachedByMountpoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
+ $cachedByMountPoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
+
+ $format = $input->getOption('output');
- foreach ($mounts as $mount) {
- $output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId());
- if (isset($cachedByMountpoint[$mount->getMountPoint()])) {
- $cached = $cachedByMountpoint[$mount->getMountPoint()];
- $output->writeln("\t- provider: " . $cached->getMountProvider());
- $output->writeln("\t- storage id: " . $cached->getStorageId());
- $output->writeln("\t- root id: " . $cached->getRootId());
- } else {
- $output->writeln("\tnot registered");
+ if ($format === self::OUTPUT_FORMAT_PLAIN) {
+ foreach ($mounts as $mount) {
+ $output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId());
+ if (isset($cachedByMountPoint[$mount->getMountPoint()])) {
+ $cached = $cachedByMountPoint[$mount->getMountPoint()];
+ $output->writeln("\t- provider: " . $cached->getMountProvider());
+ $output->writeln("\t- storage id: " . $cached->getStorageId());
+ $output->writeln("\t- root id: " . $cached->getRootId());
+ } else {
+ $output->writeln("\tnot registered");
+ }
}
- }
- foreach ($cachedMounts as $cachedMount) {
- if (!isset($mountsByMountpoint[$cachedMount->getMountPoint()])) {
- $output->writeln('' . $cachedMount->getMountPoint() . ':');
- $output->writeln("\tregistered but no longer provided");
- $output->writeln("\t- provider: " . $cachedMount->getMountProvider());
- $output->writeln("\t- storage id: " . $cachedMount->getStorageId());
- $output->writeln("\t- root id: " . $cachedMount->getRootId());
+ foreach ($cachedMounts as $cachedMount) {
+ if ($cachedOnly || !isset($mountsByMountPoint[$cachedMount->getMountPoint()])) {
+ $output->writeln('' . $cachedMount->getMountPoint() . ':');
+ if (!$cachedOnly) {
+ $output->writeln("\tregistered but no longer provided");
+ }
+ $output->writeln("\t- provider: " . $cachedMount->getMountProvider());
+ $output->writeln("\t- storage id: " . $cachedMount->getStorageId());
+ $output->writeln("\t- root id: " . $cachedMount->getRootId());
+ }
}
+ } else {
+ $cached = array_map(fn (ICachedMountInfo $cachedMountInfo) => [
+ 'mountpoint' => $cachedMountInfo->getMountPoint(),
+ 'provider' => $cachedMountInfo->getMountProvider(),
+ 'storage_id' => $cachedMountInfo->getStorageId(),
+ 'root_id' => $cachedMountInfo->getRootId(),
+ ], $cachedMounts);
+ $provided = array_map(fn (IMountPoint $cachedMountInfo) => [
+ 'mountpoint' => $cachedMountInfo->getMountPoint(),
+ 'provider' => $cachedMountInfo->getMountProvider(),
+ 'storage_id' => $cachedMountInfo->getStorageId(),
+ 'root_id' => $cachedMountInfo->getStorageRootId(),
+ ], $mounts);
+ $this->writeArrayInOutputFormat($input, $output, array_filter([
+ 'cached' => $cached,
+ 'provided' => $cachedOnly ? null : $provided,
+ ]));
}
-
return 0;
}
diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php
index 5ee25e98ea8b6..3864710ba3d88 100644
--- a/apps/files/lib/Service/OwnershipTransferService.php
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -577,14 +577,16 @@ private function restoreShares(
$output->writeln('');
}
- private function transferIncomingShares(string $sourceUid,
+ private function transferIncomingShares(
+ string $sourceUid,
string $destinationUid,
array $sourceShares,
array $destinationShares,
OutputInterface $output,
string $path,
string $finalTarget,
- bool $move): void {
+ bool $move,
+ ): void {
$output->writeln('Restoring incoming shares ...');
$progress = new ProgressBar($output, count($sourceShares));
$prefix = "$destinationUid/files";
@@ -623,8 +625,11 @@ private function transferIncomingShares(string $sourceUid,
if ($move) {
continue;
}
+ $oldMountPoint = $this->getShareMountPoint($destinationUid, $share->getTarget());
+ $newMountPoint = $this->getShareMountPoint($destinationUid, $shareTarget);
$share->setTarget($shareTarget);
$this->shareManager->moveShare($share, $destinationUid);
+ $this->mountManager->moveMount($oldMountPoint, $newMountPoint);
continue;
}
$this->shareManager->deleteShare($share);
@@ -642,8 +647,11 @@ private function transferIncomingShares(string $sourceUid,
if ($move) {
continue;
}
+ $oldMountPoint = $this->getShareMountPoint($destinationUid, $share->getTarget());
+ $newMountPoint = $this->getShareMountPoint($destinationUid, $shareTarget);
$share->setTarget($shareTarget);
$this->shareManager->moveShare($share, $destinationUid);
+ $this->mountManager->moveMount($oldMountPoint, $newMountPoint);
continue;
}
} catch (NotFoundException $e) {
@@ -656,4 +664,8 @@ private function transferIncomingShares(string $sourceUid,
$progress->finish();
$output->writeln('');
}
+
+ private function getShareMountPoint(string $uid, string $target): string {
+ return '/' . $uid . '/files/' . trim($target, '/') . '/';
+ }
}
diff --git a/apps/files_external/lib/Service/MountCacheService.php b/apps/files_external/lib/Service/MountCacheService.php
index 2be17b9d59863..119b7b6f8b6ec 100644
--- a/apps/files_external/lib/Service/MountCacheService.php
+++ b/apps/files_external/lib/Service/MountCacheService.php
@@ -75,7 +75,7 @@ public function handle(Event $event): void {
public function handleDeletedStorage(StorageConfig $storage): void {
foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) {
- $this->userMountCache->removeMount($storage->getMountPointForUser($user));
+ $this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
}
}
@@ -87,7 +87,7 @@ public function handleAddedStorage(StorageConfig $storage): void {
public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void {
foreach ($this->applicableHelper->diffApplicable($oldStorage, $newStorage) as $user) {
- $this->userMountCache->removeMount($oldStorage->getMountPointForUser($user));
+ $this->userMountCache->removeMount($oldStorage->getMountPointForUser($user), $user);
}
foreach ($this->applicableHelper->diffApplicable($newStorage, $oldStorage) as $user) {
$this->registerForUser($user, $newStorage);
@@ -156,7 +156,7 @@ private function handleUserRemoved(IGroup $group, IUser $user): void {
$storages = $this->storagesService->getAllStoragesForGroup($group);
foreach ($storages as $storage) {
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
- $this->userMountCache->removeMount($storage->getMountPointForUser($user));
+ $this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
}
}
}
@@ -181,7 +181,7 @@ private function handleGroupDeleted(IGroup $group): void {
private function removeGroupFromStorage(StorageConfig $storage, IGroup $group): void {
foreach ($group->searchUsers('') as $user) {
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
- $this->userMountCache->removeMount($storage->getMountPointForUser($user));
+ $this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
}
}
}
diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php
index 5962eab0d5f50..2d6b545a17a63 100644
--- a/apps/files_sharing/composer/composer/autoload_classmap.php
+++ b/apps/files_sharing/composer/composer/autoload_classmap.php
@@ -72,6 +72,7 @@
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
+ 'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => $baseDir . '/../lib/Listener/UserHomeSetupListener.php',
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php',
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php',
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => $baseDir . '/../lib/Middleware/ShareInfoMiddleware.php',
@@ -96,6 +97,7 @@
'OCA\\Files_Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\Files_Sharing\\Scanner' => $baseDir . '/../lib/Scanner.php',
'OCA\\Files_Sharing\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
+ 'OCA\\Files_Sharing\\ShareRecipientUpdater' => $baseDir . '/../lib/ShareRecipientUpdater.php',
'OCA\\Files_Sharing\\ShareTargetValidator' => $baseDir . '/../lib/ShareTargetValidator.php',
'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php',
diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php
index 51895e2be730a..1fc2ddd2dfd8a 100644
--- a/apps/files_sharing/composer/composer/autoload_static.php
+++ b/apps/files_sharing/composer/composer/autoload_static.php
@@ -87,6 +87,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
+ 'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => __DIR__ . '/..' . '/../lib/Listener/UserHomeSetupListener.php',
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php',
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php',
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ShareInfoMiddleware.php',
@@ -111,6 +112,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\Files_Sharing\\Scanner' => __DIR__ . '/..' . '/../lib/Scanner.php',
'OCA\\Files_Sharing\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
+ 'OCA\\Files_Sharing\\ShareRecipientUpdater' => __DIR__ . '/..' . '/../lib/ShareRecipientUpdater.php',
'OCA\\Files_Sharing\\ShareTargetValidator' => __DIR__ . '/..' . '/../lib/ShareTargetValidator.php',
'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php',
diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php
index d50aaa71e8f2b..8f5c4c4c4617a 100644
--- a/apps/files_sharing/lib/AppInfo/Application.php
+++ b/apps/files_sharing/lib/AppInfo/Application.php
@@ -26,6 +26,7 @@
use OCA\Files_Sharing\Listener\ShareInteractionListener;
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
+use OCA\Files_Sharing\Listener\UserHomeSetupListener;
use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
use OCA\Files_Sharing\Middleware\ShareInfoMiddleware;
@@ -45,6 +46,8 @@
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\Events\BeforeZipCreatedEvent;
use OCP\Files\Events\Node\BeforeNodeReadEvent;
+use OCP\Files\Events\UserHomeSetupEvent;
+use OCP\Group\Events\BeforeGroupDeletedEvent;
use OCP\Group\Events\GroupChangedEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
@@ -54,6 +57,7 @@
use OCP\IGroup;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareMovedEvent;
use OCP\Share\Events\ShareTransferredEvent;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
@@ -119,7 +123,11 @@ function () use ($c) {
$context->registerEventListener(ShareTransferredEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserAddedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserRemovedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(BeforeGroupDeletedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(GroupDeletedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserShareAccessUpdatedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(ShareMovedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);
$context->registerConfigLexicon(ConfigLexicon::class);
}
diff --git a/apps/files_sharing/lib/Config/ConfigLexicon.php b/apps/files_sharing/lib/Config/ConfigLexicon.php
index c063153765e26..e34010edf79ac 100644
--- a/apps/files_sharing/lib/Config/ConfigLexicon.php
+++ b/apps/files_sharing/lib/Config/ConfigLexicon.php
@@ -24,6 +24,8 @@ class ConfigLexicon implements ILexicon {
public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal';
public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal';
public const EXCLUDE_RESHARE_FROM_EDIT = 'shareapi_exclude_reshare_from_edit';
+ public const UPDATE_CUTOFF_TIME = 'update_cutoff_time';
+ public const USER_NEEDS_SHARE_REFRESH = 'user_needs_share_refresh';
public function getStrictness(): Strictness {
return Strictness::IGNORE;
@@ -34,10 +36,14 @@ public function getAppConfigs(): array {
new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true),
new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true),
new Entry(self::EXCLUDE_RESHARE_FROM_EDIT, ValueType::BOOL, false, 'Exclude reshare permission from "Allow editing" bundled permissions'),
+
+ new Entry(self::UPDATE_CUTOFF_TIME, ValueType::FLOAT, 3.0, 'Maximum time in second during which we update the share data immediately before switching to only marking the user'),
];
}
public function getUserConfigs(): array {
- return [];
+ return [
+ new Entry(self::USER_NEEDS_SHARE_REFRESH, ValueType::BOOL, true, 'whether a user needs to have the receiving share data refreshed for possible changes'),
+ ];
}
}
diff --git a/apps/files_sharing/lib/Listener/SharesUpdatedListener.php b/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
index ccef71bad5c87..9cb3740671051 100644
--- a/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
+++ b/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
@@ -8,93 +8,149 @@
namespace OCA\Files_Sharing\Listener;
+use OCA\Files_Sharing\AppInfo\Application;
+use OCA\Files_Sharing\Config\ConfigLexicon;
use OCA\Files_Sharing\Event\UserShareAccessUpdatedEvent;
-use OCA\Files_Sharing\MountProvider;
-use OCA\Files_Sharing\ShareTargetValidator;
+use OCA\Files_Sharing\ShareRecipientUpdater;
+use OCP\Config\IUserConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
-use OCP\Files\Config\ICachedMountInfo;
-use OCP\Files\Config\IUserMountCache;
-use OCP\Files\Storage\IStorageFactory;
+use OCP\Group\Events\BeforeGroupDeletedEvent;
+use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
+use OCP\IAppConfig;
use OCP\IUser;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareMovedEvent;
use OCP\Share\Events\ShareTransferredEvent;
use OCP\Share\IManager;
+use Psr\Clock\ClockInterface;
+use Psr\Log\LoggerInterface;
/**
* Listen to various events that can change what shares a user has access to
*
- * @template-implements IEventListener
+ * @psalm-type GroupEvents = UserAddedEvent|UserRemovedEvent|GroupDeletedEvent|BeforeGroupDeletedEvent
+ * @template-implements IEventListener
*/
class SharesUpdatedListener implements IEventListener {
- private array $inUpdate = [];
+ /**
+ * for how long do we update the share date immediately,
+ * before just marking the other users
+ */
+ private float $cutOffMarkTime;
+
+ /**
+ * The total amount of time we've spent so far processing updates
+ */
+ private float $updatedTime = 0.0;
public function __construct(
private readonly IManager $shareManager,
- private readonly IUserMountCache $userMountCache,
- private readonly MountProvider $shareMountProvider,
- private readonly ShareTargetValidator $shareTargetValidator,
- private readonly IStorageFactory $storageFactory,
+ private readonly ShareRecipientUpdater $shareUpdater,
+ private readonly IUserConfig $userConfig,
+ private readonly ClockInterface $clock,
+ private readonly LoggerInterface $logger,
+ IAppConfig $appConfig,
+ private readonly UserHomeSetupListener $homeSetupListener,
) {
+ $this->cutOffMarkTime = $appConfig->getValueFloat(Application::APP_ID, ConfigLexicon::UPDATE_CUTOFF_TIME, 3.0);
}
+
public function handle(Event $event): void {
+ // don't trigger the on-setup checks if this handler triggers an fs setup
+ $oldState = $this->homeSetupListener->setDisabled(true);
+
if ($event instanceof UserShareAccessUpdatedEvent) {
foreach ($event->getUsers() as $user) {
- $this->updateForUser($user, true);
+ $this->updateOrMarkUser($user);
}
}
- if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
- $this->updateForUser($event->getUser(), true);
+ if ($event instanceof BeforeGroupDeletedEvent) {
+ // ensure the group users are loaded before the group is deleted
+ // `IGroup::getUsers` does internal caching, so we don't need to store this separately
+ $event->getGroup()->getUsers();
}
- if (
- $event instanceof ShareCreatedEvent
- || $event instanceof ShareTransferredEvent
- ) {
- foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
- $this->updateForUser($user, true);
+ if ($event instanceof GroupDeletedEvent) {
+ // so we can iterate them after the group is deleted
+ foreach ($event->getGroup()->getUsers() as $user) {
+ $this->updateOrMarkUser($user);
}
}
- if ($event instanceof BeforeShareDeletedEvent) {
- foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
- $this->updateForUser($user, false, [$event->getShare()]);
+ if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
+ $this->updateOrMarkUser($event->getUser());
+ }
+ if ($event instanceof ShareCreatedEvent || $event instanceof ShareTransferredEvent) {
+ $share = $event->getShare();
+ $shareTarget = $share->getTarget();
+ foreach ($this->shareManager->getUsersForShare($share) as $user) {
+ if ($share->getShareOwner() === $user->getUID() || $share->getSharedBy() === $user->getUID()) {
+ continue;
+ }
+
+ if ($share->getSharedBy() !== $user->getUID()) {
+ $this->markOrRun($user, function () use ($user, $share) {
+ $this->shareUpdater->updateForAddedShare($user, $share);
+ });
+ // Share target validation might have changed the target, restore it for the next user
+ $share->setTarget($shareTarget);
+ }
}
}
- }
+ if ($event instanceof ShareMovedEvent) {
+ $share = $event->getShare();
+ $user = $event->getUser();
- private function updateForUser(IUser $user, bool $verifyMountPoints, array $ignoreShares = []): void {
- // prevent recursion
- if (isset($this->inUpdate[$user->getUID()])) {
- return;
+ // don't trigger if the share is moved as part of the conflict resolution
+ if (!$this->shareUpdater->isInUpdate($user)) {
+ $this->markOrRun($user, function () use ($user, $share) {
+ $this->shareUpdater->updateForMovedShare($user, $share);
+ });
+ }
}
- $this->inUpdate[$user->getUID()] = true;
- $cachedMounts = $this->userMountCache->getMountsForUser($user);
- $shareMounts = array_filter($cachedMounts, fn (ICachedMountInfo $mount) => $mount->getMountProvider() === MountProvider::class);
- $mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
- $mountsByPath = array_combine($mountPoints, $cachedMounts);
-
- $shares = $this->shareMountProvider->getSuperSharesForUser($user, $ignoreShares);
-
- $mountsChanged = count($shares) !== count($shareMounts);
- foreach ($shares as &$share) {
- [$parentShare, $groupedShares] = $share;
- $mountPoint = '/' . $user->getUID() . '/files/' . trim($parentShare->getTarget(), '/') . '/';
- $mountKey = $parentShare->getNodeId() . '::' . $mountPoint;
- if (!isset($cachedMounts[$mountKey])) {
- $mountsChanged = true;
- if ($verifyMountPoints) {
- $this->shareTargetValidator->verifyMountPoint($user, $parentShare, $mountsByPath, $groupedShares);
+ if ($event instanceof BeforeShareDeletedEvent) {
+ $share = $event->getShare();
+ foreach ($this->shareManager->getUsersForShare($share) as $user) {
+ if ($share->getShareOwner() === $user->getUID() || $share->getSharedBy() === $user->getUID()) {
+ continue;
}
+
+ $this->markOrRun($user, function () use ($user, $share) {
+ $this->shareUpdater->updateForDeletedShare($user, $share);
+ });
}
}
- if ($mountsChanged) {
- $newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
- $this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
+ $this->homeSetupListener->setDisabled($oldState);
+ }
+
+ private function markOrRun(IUser $user, callable $callback): void {
+ $start = floatval($this->clock->now()->format('U.u'));
+ if ($this->cutOffMarkTime === -1.0 || $this->updatedTime < $this->cutOffMarkTime) {
+ $callback();
+ } else {
+ $this->markUserForRefresh($user);
}
+ $end = floatval($this->clock->now()->format('U.u'));
+ $this->updatedTime += $end - $start;
+ }
+
+ private function updateOrMarkUser(IUser $user): void {
+ $this->markOrRun($user, function () use ($user) {
+ $this->shareUpdater->updateForUser($user);
+ });
+ }
+
+ private function markUserForRefresh(IUser $user): void {
+ // log with exception to capture the trace
+ $ex = new \Exception('Marking ' . $user->getUID() . ' as needing the share mounts refreshed');
+ $this->logger->debug($ex->getMessage(), ['exception' => $ex]);
+ $this->userConfig->setValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true);
+ }
- unset($this->inUpdate[$user->getUID()]);
+ public function setCutOffMarkTime(float|int $cutOffMarkTime): void {
+ $this->cutOffMarkTime = (float)$cutOffMarkTime;
}
}
diff --git a/apps/files_sharing/lib/Listener/UserHomeSetupListener.php b/apps/files_sharing/lib/Listener/UserHomeSetupListener.php
new file mode 100644
index 0000000000000..7f95605b3b601
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/UserHomeSetupListener.php
@@ -0,0 +1,53 @@
+
+ */
+class UserHomeSetupListener implements IEventListener {
+ private bool $disabled = false;
+ public function __construct(
+ private readonly ShareRecipientUpdater $updater,
+ private readonly IUserConfig $userConfig,
+ ) {
+ }
+
+ public function setDisabled(bool $disabled): bool {
+ $previous = $this->disabled;
+ $this->disabled = $disabled;
+ return $previous;
+ }
+ public function handle(Event $event): void {
+ if (!$event instanceof UserHomeSetupEvent) {
+ return;
+ }
+ if ($this->disabled) {
+ return;
+ }
+
+ $user = $event->getUser();
+ if ($this->userConfig->getValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true)) {
+ $this->updater->updateForUser($user);
+ $this->userConfig->setValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, false);
+ }
+ }
+
+}
diff --git a/apps/files_sharing/lib/Repair/CleanupShareTarget.php b/apps/files_sharing/lib/Repair/CleanupShareTarget.php
index 60a62c7add11d..880c2a15af6c6 100644
--- a/apps/files_sharing/lib/Repair/CleanupShareTarget.php
+++ b/apps/files_sharing/lib/Repair/CleanupShareTarget.php
@@ -107,7 +107,7 @@ public function run(IOutput $output) {
(int)$shareInfo['file_source'],
$absoluteNewTarget,
$targetParentNode->getMountPoint(),
- $userMounts,
+ fn ($path) => $userMounts[$path] ?? null,
);
$newTarget = $userFolder->getRelativePath($absoluteNewTarget);
diff --git a/apps/files_sharing/lib/ShareRecipientUpdater.php b/apps/files_sharing/lib/ShareRecipientUpdater.php
new file mode 100644
index 0000000000000..144bdc7bc947b
--- /dev/null
+++ b/apps/files_sharing/lib/ShareRecipientUpdater.php
@@ -0,0 +1,113 @@
+isInUpdate($user)) {
+ return;
+ }
+ $this->inUpdate[$user->getUID()] = true;
+
+ $cachedMounts = $this->userMountCache->getMountsForUser($user);
+ $shareMounts = array_filter($cachedMounts, fn (ICachedMountInfo $mount) => $mount->getMountProvider() === MountProvider::class);
+ $mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
+ $mountsByPath = array_combine($mountPoints, $cachedMounts);
+
+ $shares = $this->shareMountProvider->getSuperSharesForUser($user);
+
+ // the share mounts have changed if either the number of shares doesn't matched the number of share mounts
+ // or there is a share for which we don't have a mount yet.
+ $mountsChanged = count($shares) !== count($shareMounts);
+ foreach ($shares as $share) {
+ [$parentShare, $groupedShares] = $share;
+ $mountPoint = $this->getMountPointFromTarget($user, $parentShare->getTarget());
+ $mountKey = $parentShare->getNodeId() . '::' . $mountPoint;
+ if (!isset($cachedMounts[$mountKey])) {
+ $mountsChanged = true;
+ $this->shareTargetValidator->verifyMountPoint($user, $parentShare, fn ($path) => $mountsByPath[$path] ?? null, $groupedShares);
+ }
+ }
+
+ if ($mountsChanged) {
+ $newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
+ $this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
+ }
+
+ unset($this->inUpdate[$user->getUID()]);
+ }
+
+ public function isInUpdate(IUser $user): bool {
+ return isset($this->inUpdate[$user->getUID()]);
+ }
+
+ /**
+ * Validate a single received share for a user
+ */
+ public function updateForAddedShare(IUser $user, IShare $share): void {
+ $target = $this->shareTargetValidator->verifyMountPoint($user, $share, fn ($path) => $this->userMountCache->getMountAtPath($user, $path), [$share]);
+ $mountPoint = $this->getMountPointFromTarget($user, $target);
+
+ $this->userMountCache->addMount($user, $mountPoint, $share->getNode()->getData(), MountProvider::class);
+ }
+
+ private function getMountPointFromTarget(IUser $user, string $target): string {
+ return '/' . $user->getUID() . '/files/' . trim($target, '/') . '/';
+ }
+
+ /**
+ * Process a single deleted share for a user
+ */
+ public function updateForDeletedShare(IUser $user, IShare $share): void {
+ try {
+ $userShare = $this->shareManager->getShareById($share->getFullId(), $user->getUID(), false);
+ $this->userMountCache->removeMount($this->getMountPointFromTarget($user, $userShare->getTarget()), $user);
+ } catch (ShareNotFound) {
+ // user doesn't actually have access to the share
+ }
+ }
+
+ /**
+ * Process a single moved share for a user
+ */
+ public function updateForMovedShare(IUser $user, IShare $share): void {
+ $originalTarget = $share->getOriginalTarget();
+ if ($originalTarget != null) {
+ $newMountPoint = $this->getMountPointFromTarget($user, $share->getTarget());
+ $oldMountPoint = $this->getMountPointFromTarget($user, $originalTarget);
+ $this->userMountCache->removeMount($oldMountPoint, $user);
+ $this->userMountCache->addMount($user, $newMountPoint, $share->getNode()->getData(), MountProvider::class);
+ } else {
+ $this->updateForUser($user);
+ }
+ }
+}
diff --git a/apps/files_sharing/lib/ShareTargetValidator.php b/apps/files_sharing/lib/ShareTargetValidator.php
index e1c8c0345cdd5..e77dc5ddad07a 100644
--- a/apps/files_sharing/lib/ShareTargetValidator.php
+++ b/apps/files_sharing/lib/ShareTargetValidator.php
@@ -13,8 +13,7 @@
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\ICachedMountInfo;
-use OCP\Files\ISetupManager;
-use OCP\Files\Mount\IMountManager;
+use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\IUser;
use OCP\Share\Events\VerifyMountPointEvent;
@@ -30,8 +29,7 @@ class ShareTargetValidator {
public function __construct(
private readonly IManager $shareManager,
private readonly IEventDispatcher $eventDispatcher,
- private readonly ISetupManager $setupManager,
- private readonly IMountManager $mountManager,
+ private readonly IRootFolder $rootFolder,
) {
$this->folderExistsCache = new CappedMemoryCache();
}
@@ -47,14 +45,14 @@ private function getViewForUser(IUser $user): View {
/**
* check if the parent folder exists otherwise move the mount point up
*
- * @param array $allCachedMounts Other mounts for the user, indexed by path
+ * @param callable(string):?ICachedMountInfo $getMountByPath
* @param IShare[] $childShares
* @return string
*/
public function verifyMountPoint(
IUser $user,
IShare &$share,
- array $allCachedMounts,
+ callable $getMountByPath,
array $childShares,
): string {
$mountPoint = basename($share->getTarget());
@@ -67,8 +65,10 @@ public function verifyMountPoint(
/** @psalm-suppress InternalMethod */
$absoluteParent = $recipientView->getAbsolutePath($parent);
- $this->setupManager->setupForPath($absoluteParent);
- $parentMount = $this->mountManager->find($absoluteParent);
+
+ // the share target always has to be in the users home
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $parentMount = $userFolder->getMountPoint();
$cached = $this->folderExistsCache->get($parent);
if ($cached) {
@@ -94,7 +94,7 @@ public function verifyMountPoint(
$share->getNodeId(),
Filesystem::normalizePath($absoluteParent . '/' . $mountPoint),
$parentMount,
- $allCachedMounts,
+ $getMountByPath,
);
/** @psalm-suppress InternalMethod */
@@ -112,13 +112,13 @@ public function verifyMountPoint(
/**
- * @param ICachedMountInfo[] $allCachedMounts
+ * @param callable(string):?ICachedMountInfo $getMountByPath
*/
public function generateUniqueTarget(
int $shareNodeId,
string $absolutePath,
IMountPoint $parentMount,
- array $allCachedMounts,
+ callable $getMountByPath,
): string {
$pathInfo = pathinfo($absolutePath);
$ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
@@ -128,7 +128,7 @@ public function generateUniqueTarget(
$i = 2;
$parentCache = $parentMount->getStorage()->getCache();
$internalPath = $parentMount->getInternalPath($absolutePath);
- while ($parentCache->inCache($internalPath) || $this->hasConflictingMount($shareNodeId, $allCachedMounts, $absolutePath)) {
+ while ($parentCache->inCache($internalPath) || $this->hasConflictingMount($shareNodeId, $getMountByPath, $absolutePath)) {
$absolutePath = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
$internalPath = $parentMount->getInternalPath($absolutePath);
$i++;
@@ -138,14 +138,14 @@ public function generateUniqueTarget(
}
/**
- * @param ICachedMountInfo[] $allCachedMounts
+ * @param callable(string):?ICachedMountInfo $getMountByPath
*/
- private function hasConflictingMount(int $shareNodeId, array $allCachedMounts, string $absolutePath): bool {
- if (!isset($allCachedMounts[$absolutePath . '/'])) {
+ private function hasConflictingMount(int $shareNodeId, callable $getMountByPath, string $absolutePath): bool {
+ $mount = $getMountByPath($absolutePath . '/');
+ if ($mount === null) {
return false;
}
- $mount = $allCachedMounts[$absolutePath . '/'];
if ($mount->getMountProvider() === MountProvider::class && $mount->getRootId() === $shareNodeId) {
// "conflicting" mount is a mount for the current share
return false;
diff --git a/apps/files_sharing/tests/Listener/UserHomeSetupListenerTest.php b/apps/files_sharing/tests/Listener/UserHomeSetupListenerTest.php
new file mode 100644
index 0000000000000..4042b7b8971c8
--- /dev/null
+++ b/apps/files_sharing/tests/Listener/UserHomeSetupListenerTest.php
@@ -0,0 +1,62 @@
+updater = $this->createMock(ShareRecipientUpdater::class);
+ $this->userConfig = new MockUserConfig([]);
+ $this->listener = new UserHomeSetupListener($this->updater, $this->userConfig);
+ $this->user = $this->createMock(IUser::class);
+ $this->user->method('getUID')
+ ->willReturn('test');
+ }
+
+ private function getEvent(): UserHomeSetupEvent {
+ $homeMount = $this->createMock(IMountPoint::class);
+ return new UserHomeSetupEvent($this->user, $homeMount);
+ }
+
+ public function testClearNeedsUpdate(): void {
+ $this->userConfig->setValueBool('test', Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true);
+ $this->updater->expects($this->once())
+ ->method('updateForUser');
+
+ $this->listener->handle($this->getEvent());
+ $this->assertFalse($this->userConfig->getValueBool('test', Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true));
+ }
+
+ public function testNoUpdateIfNotNeeded(): void {
+ $this->userConfig->setValueBool('test', Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, false);
+ $this->updater->expects($this->never())
+ ->method('updateForUser');
+
+ $this->listener->handle($this->getEvent());
+ $this->assertFalse($this->userConfig->getValueBool('test', Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true));
+ }
+}
diff --git a/apps/files_sharing/tests/ShareRecipientUpdaterTest.php b/apps/files_sharing/tests/ShareRecipientUpdaterTest.php
new file mode 100644
index 0000000000000..ed365385d8df2
--- /dev/null
+++ b/apps/files_sharing/tests/ShareRecipientUpdaterTest.php
@@ -0,0 +1,216 @@
+userMountCache = $this->createMock(IUserMountCache::class);
+ $this->shareMountProvider = $this->createMock(MountProvider::class);
+ $this->shareTargetValidator = $this->createMock(ShareTargetValidator::class);
+ $this->storageFactory = $this->createMock(IStorageFactory::class);
+ $this->shareManager = $this->createMock(IManager::class);
+
+ $this->updater = new ShareRecipientUpdater(
+ $this->userMountCache,
+ $this->shareMountProvider,
+ $this->shareTargetValidator,
+ $this->storageFactory,
+ $this->shareManager,
+ );
+ }
+
+ public function testUpdateForShare() {
+ $share = $this->createMock(IShare::class);
+ $node = $this->createMock(Node::class);
+ $cacheEntry = $this->createMock(ICacheEntry::class);
+ $share->method('getNode')
+ ->willReturn($node);
+ $node->method('getData')
+ ->willReturn($cacheEntry);
+ $user1 = $this->createUser('user1', '');
+
+ $this->userMountCache->method('getMountsForUser')
+ ->with($user1)
+ ->willReturn([]);
+
+ $this->shareTargetValidator->method('verifyMountPoint')
+ ->with($user1, $share, fn ($path) => null, [$share])
+ ->willReturn('/new-target');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('addMount')
+ ->with($user1, '/user1/files/new-target/', $cacheEntry, MountProvider::class);
+
+ $this->updater->updateForAddedShare($user1, $share);
+ }
+
+ /**
+ * @param IUser $user
+ * @param list $mounts
+ * @return void
+ */
+ private function setCachedMounts(IUser $user, array $mounts) {
+ $cachedMounts = array_map(function (array $mount): ICachedMountInfo {
+ $cachedMount = $this->createMock(ICachedMountInfo::class);
+ $cachedMount->method('getRootId')
+ ->willReturn($mount['fileid']);
+ $cachedMount->method('getMountPoint')
+ ->willReturn($mount['mount_point']);
+ $cachedMount->method('getMountProvider')
+ ->willReturn($mount['provider']);
+ return $cachedMount;
+ }, $mounts);
+ $mountKeys = array_map(function (array $mount): string {
+ return $mount['fileid'] . '::' . $mount['mount_point'];
+ }, $mounts);
+
+ $this->userMountCache->method('getMountsForUser')
+ ->with($user)
+ ->willReturn(array_combine($mountKeys, $cachedMounts));
+ }
+
+ public function testUpdateForUserAddedNoExisting() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+ $newMount = $this->createMock(IMountPoint::class);
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([[
+ $share,
+ [$share],
+ ]]);
+
+ $this->shareMountProvider->method('getMountsFromSuperShares')
+ ->with($user1, [[
+ $share,
+ [$share],
+ ]], $this->storageFactory)
+ ->willReturn([$newMount]);
+
+ $this->setCachedMounts($user1, []);
+
+ $this->shareTargetValidator->method('verifyMountPoint')
+ ->with($user1, $share, fn ($path) => null, [$share])
+ ->willReturn('/new-target');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('registerMounts')
+ ->with($user1, [$newMount], [MountProvider::class]);
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testUpdateForUserNoChanges() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([[
+ $share,
+ [$share],
+ ]]);
+
+ $this->setCachedMounts($user1, [
+ ['fileid' => 111, 'mount_point' => '/user1/files/target/', 'provider' => MountProvider::class],
+ ]);
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->never())
+ ->method('registerMounts');
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testUpdateForUserRemoved() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([]);
+
+ $this->setCachedMounts($user1, [
+ ['fileid' => 111, 'mount_point' => '/user1/files/target/', 'provider' => MountProvider::class],
+ ]);
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('registerMounts')
+ ->with($user1, [], [MountProvider::class]);
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testDeletedShare() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $share->method('getFullId')
+ ->willReturn('id');
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareManager->method('getShareById')
+ ->with('id')
+ ->willReturn($share);
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('removeMount')
+ ->with('/user1/files/target/');
+
+ $this->updater->updateForDeletedShare($user1, $share);
+ }
+}
diff --git a/apps/files_sharing/tests/ShareTargetValidatorTest.php b/apps/files_sharing/tests/ShareTargetValidatorTest.php
index 7bc5820d39e66..1c5b8b619c27a 100644
--- a/apps/files_sharing/tests/ShareTargetValidatorTest.php
+++ b/apps/files_sharing/tests/ShareTargetValidatorTest.php
@@ -9,12 +9,11 @@
namespace OCA\Files_Sharing\Tests;
use OC\EventDispatcher\EventDispatcher;
-use OC\Files\SetupManager;
use OCA\Files_Sharing\ShareTargetValidator;
use OCP\Constants;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Config\ICachedMountInfo;
-use OCP\Files\Mount\IMountManager;
+use OCP\Files\IRootFolder;
use OCP\IUser;
use OCP\Server;
use OCP\Share\Events\VerifyMountPointEvent;
@@ -57,8 +56,7 @@ protected function setUp(): void {
$this->targetValidator = new ShareTargetValidator(
Server::get(IManager::class),
$this->eventDispatcher,
- Server::get(SetupManager::class),
- Server::get(IMountManager::class),
+ Server::get(IRootFolder::class),
);
$this->user2 = $this->createMock(IUser::class);
$this->user2->method('getUID')
@@ -85,7 +83,7 @@ public function testShareMountLoseParentFolder(): void {
$share = $this->shareManager->getShareById($share->getFullId());
$this->assertSame('/foo/bar' . $this->folder, $share->getTarget());
- $this->targetValidator->verifyMountPoint($this->user2, $share, [], [$share]);
+ $this->targetValidator->verifyMountPoint($this->user2, $share, fn ($path) => null, [$share]);
$share = $this->shareManager->getShareById($share->getFullId());
$this->assertSame($this->folder, $share->getTarget());
@@ -119,7 +117,7 @@ public function testShareMountOverFolder(): void {
$share = $this->shareManager->getShareById($share->getFullId());
- $this->targetValidator->verifyMountPoint($this->user2, $share, [], [$share]);
+ $this->targetValidator->verifyMountPoint($this->user2, $share, fn ($path) => null, [$share]);
$share = $this->shareManager->getShareById($share->getFullId());
$this->assertSame('/bar (2)', $share->getTarget());
@@ -144,9 +142,10 @@ public function testShareMountOverShare(): void {
$this->shareManager->acceptShare($share2, self::TEST_FILES_SHARING_API_USER2);
$conflictingMount = $this->createMock(ICachedMountInfo::class);
- $this->targetValidator->verifyMountPoint($this->user2, $share2, [
+ $conflictingMounts = [
'/' . $this->user2->getUID() . '/files' . $this->folder2 . '/' => $conflictingMount
- ], [$share2]);
+ ];
+ $this->targetValidator->verifyMountPoint($this->user2, $share2, fn ($path) => $conflictingMounts[$path] ?? null, [$share2]);
$share2 = $this->shareManager->getShareById($share2->getFullId());
@@ -181,7 +180,7 @@ public function testShareMountCreateParentFolder(): void {
$this->eventDispatcher->addListener(VerifyMountPointEvent::class, function (VerifyMountPointEvent $event): void {
$event->setCreateParent(true);
});
- $this->targetValidator->verifyMountPoint($this->user2, $share, [], [$share]);
+ $this->targetValidator->verifyMountPoint($this->user2, $share, fn ($path) => null, [$share]);
$share = $this->shareManager->getShareById($share->getFullId());
$this->assertSame('/foo/bar' . $this->folder, $share->getTarget());
diff --git a/apps/files_sharing/tests/SharesUpdatedListenerTest.php b/apps/files_sharing/tests/SharesUpdatedListenerTest.php
new file mode 100644
index 0000000000000..3598f6cfb5b09
--- /dev/null
+++ b/apps/files_sharing/tests/SharesUpdatedListenerTest.php
@@ -0,0 +1,191 @@
+shareRecipientUpdater = $this->createMock(ShareRecipientUpdater::class);
+ $this->manager = $this->createMock(IManager::class);
+ $this->appConfig = new MockAppConfig([
+ ConfigLexicon::UPDATE_CUTOFF_TIME => -1,
+ ]);
+ $this->userConfig = new MockUserConfig();
+ $this->clock = $this->createMock(ClockInterface::class);
+ $this->clockFn = function () {
+ return new \DateTimeImmutable('@0');
+ };
+ $this->clock->method('now')
+ ->willReturnCallback(function () {
+ // extra wrapper so we can modify clockFn
+ return ($this->clockFn)();
+ });
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $homeSetupListener = new UserHomeSetupListener($this->shareRecipientUpdater, $this->userConfig);
+
+ $this->sharesUpdatedListener = new SharesUpdatedListener(
+ $this->manager,
+ $this->shareRecipientUpdater,
+ $this->userConfig,
+ $this->clock,
+ $this->logger,
+ $this->appConfig,
+ $homeSetupListener,
+ );
+ }
+
+ public function testShareAdded() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForAddedShare')
+ ->willReturnCallback(function (IUser $user, IShare $eventShare) use ($user1, $user2, $share) {
+ $this->assertContains($user, [$user1, $user2]);
+ $this->assertEquals($share, $eventShare);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareAddedFilterOwner() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+ $share->method('getSharedBy')
+ ->willReturn($user1->getUID());
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(1))
+ ->method('updateForAddedShare')
+ ->willReturnCallback(function (IUser $user, IShare $eventShare) use ($user2, $share) {
+ $this->assertEquals($user, $user2);
+ $this->assertEquals($share, $eventShare);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareAccessUpdated() {
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $event = new UserShareAccessUpdatedEvent([$user1, $user2]);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForUser')
+ ->willReturnCallback(function (IUser $user) use ($user1, $user2) {
+ $this->assertContains($user, [$user1, $user2]);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareDeleted() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new BeforeShareDeletedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForDeletedShare')
+ ->willReturnCallback(function (IUser $user) use ($user1, $user2, $share) {
+ $this->assertContains($user, [$user1, $user2]);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public static function shareMarkAfterTimeProvider(): array {
+ // note that each user will take exactly 1s in this test
+ return [
+ [0, 0],
+ [0.9, 1],
+ [1.1, 2],
+ [-1, 2],
+ ];
+ }
+
+ #[DataProvider('shareMarkAfterTimeProvider')]
+ public function testShareMarkAfterTime(float $cutOff, int $expectedCount) {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->sharesUpdatedListener->setCutOffMarkTime($cutOff);
+ $time = 0;
+ $this->clockFn = function () use (&$time) {
+ $time++;
+ return new \DateTimeImmutable('@' . $time);
+ };
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly($expectedCount))
+ ->method('updateForAddedShare');
+
+ $this->sharesUpdatedListener->handle($event);
+
+ $this->assertEquals($expectedCount < 1, $this->userConfig->getValueBool($user1->getUID(), 'files_sharing', ConfigLexicon::USER_NEEDS_SHARE_REFRESH));
+ $this->assertEquals($expectedCount < 2, $this->userConfig->getValueBool($user2->getUID(), 'files_sharing', ConfigLexicon::USER_NEEDS_SHARE_REFRESH));
+ }
+}
diff --git a/apps/files_sharing/tests/TestCase.php b/apps/files_sharing/tests/TestCase.php
index 6b72ecb259cab..02ee66d096118 100644
--- a/apps/files_sharing/tests/TestCase.php
+++ b/apps/files_sharing/tests/TestCase.php
@@ -15,6 +15,7 @@
use OC\User\DisplayNameCache;
use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\External\MountProvider as ExternalMountProvider;
+use OCA\Files_Sharing\Listener\SharesUpdatedListener;
use OCA\Files_Sharing\MountProvider;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\IRootFolder;
@@ -99,6 +100,8 @@ public static function setUpBeforeClass(): void {
$groupBackend->addToGroup(self::TEST_FILES_SHARING_API_USER4, 'group3');
$groupBackend->addToGroup(self::TEST_FILES_SHARING_API_USER2, self::TEST_FILES_SHARING_API_GROUP1);
Server::get(IGroupManager::class)->addBackend($groupBackend);
+
+ Server::get(SharesUpdatedListener::class)->setCutOffMarkTime(-1);
}
protected function setUp(): void {
diff --git a/apps/files_trashbin/lib/Trash/TrashItem.php b/apps/files_trashbin/lib/Trash/TrashItem.php
index 120d08bcd7c58..47fc5e9d73593 100644
--- a/apps/files_trashbin/lib/Trash/TrashItem.php
+++ b/apps/files_trashbin/lib/Trash/TrashItem.php
@@ -6,6 +6,7 @@
*/
namespace OCA\Files_Trashbin\Trash;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\IUser;
@@ -173,4 +174,8 @@ public function getDeletedBy(): ?IUser {
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}
+
+ public function getData(): ICacheEntry {
+ return $this->fileInfo->getData();
+ }
}
diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php
index 5c581c43c7227..4aca7ae697c2d 100644
--- a/build/integration/features/bootstrap/Sharing.php
+++ b/build/integration/features/bootstrap/Sharing.php
@@ -5,6 +5,7 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use PHPUnit\Framework\Assert;
@@ -13,7 +14,6 @@
require __DIR__ . '/autoload.php';
-
trait Sharing {
use Provisioning;
@@ -575,17 +575,17 @@ public function shareXIsReturnedWith(int $number, TableNode $body) {
$expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash());
if (!array_key_exists('uid_file_owner', $expectedFields)
- && array_key_exists('uid_owner', $expectedFields)) {
+ && array_key_exists('uid_owner', $expectedFields)) {
$expectedFields['uid_file_owner'] = $expectedFields['uid_owner'];
}
if (!array_key_exists('displayname_file_owner', $expectedFields)
- && array_key_exists('displayname_owner', $expectedFields)) {
+ && array_key_exists('displayname_owner', $expectedFields)) {
$expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner'];
}
if (array_key_exists('share_type', $expectedFields)
- && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */
- && array_key_exists('share_with', $expectedFields)) {
+ && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */
+ && array_key_exists('share_with', $expectedFields)) {
if ($expectedFields['share_with'] === 'private_conversation') {
$expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/';
} else {
@@ -791,4 +791,34 @@ public function getArrayOfShareesResponded(ResponseInterface $response, $shareeT
}
return $sharees;
}
+
+ /**
+ * @Then /^Share mounts for "([^"]*)" match$/
+ */
+ public function checkShareMounts(string $user, ?TableNode $body) {
+ if ($body instanceof TableNode) {
+ $fd = $body->getRows();
+
+ $expected = [];
+ foreach ($fd as $row) {
+ $expected[] = $row[0];
+ }
+ $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]);
+ $mounts = json_decode($this->lastStdOut, true)['cached'];
+ $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === \OCA\Files_Sharing\MountProvider::class);
+ $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts));
+ Assert::assertEquals($expected, $actual);
+ }
+ }
+
+ /**
+ * @Then /^Share mounts for "([^"]*)" are empty$/
+ */
+ public function checkShareMountsEmpty(string $user) {
+ $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]);
+ $mounts = json_decode($this->lastStdOut, true)['cached'];
+ $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === \OCA\Files_Sharing\MountProvider::class);
+ $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts));
+ Assert::assertEquals([], $actual);
+ }
}
diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php
index 36c76bf824462..3d13c0ae3f565 100644
--- a/build/integration/features/bootstrap/SharingContext.php
+++ b/build/integration/features/bootstrap/SharingContext.php
@@ -34,6 +34,7 @@ protected function resetAppConfigs() {
$this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares');
$this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled');
$this->deleteServerConfig('core', 'shareapi_allow_view_without_download');
+ $this->deleteServerConfig('files_sharing', 'update_cutoff_time');
$this->runOcc(['config:system:delete', 'share_folder']);
}
diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php
index fb552ce785b75..fb2e441d93791 100644
--- a/build/integration/features/bootstrap/WebDav.php
+++ b/build/integration/features/bootstrap/WebDav.php
@@ -1011,7 +1011,7 @@ public function checkIfETAGHasChanged($path, $user) {
*/
public function connectingToDavEndpoint() {
try {
- $this->response = $this->makeDavRequest(null, 'PROPFIND', '', []);
+ $this->response = $this->makeDavRequest($this->currentUser, 'PROPFIND', '', []);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature
index fb391878b9b45..02bb84750f278 100644
--- a/build/integration/sharing_features/sharing-v1-part2.feature
+++ b/build/integration/sharing_features/sharing-v1-part2.feature
@@ -77,6 +77,49 @@ Scenario: getting all shares of a file with reshares with link share with less p
| share_with | user2 |
| share_with_displayname | user2 |
+Scenario: getting all shares of a file with a received share after revoking the resharing rights with delayed share check
+ Given user "user0" exists
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user1" exists
+ And user "user2" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And user "user0" accepts last share
+ And Updating last share with
+ | permissions | 1 |
+ And file "textfile0.txt" of user "user1" is shared with user "user2"
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | share_type | 0 |
+ | uid_owner | user1 |
+ | displayname_owner | user1 |
+ | path | /textfile0 (2).txt |
+ | item_type | file |
+ | mimetype | text/plain |
+ | storage_id | shared::/textfile0 (2).txt |
+ | file_target | /textfile0.txt |
+ | share_with | user2 |
+ | share_with_displayname | user2 |
+ # After user2 does an FS setup the share is renamed
+ When As an "user2"
+ And Downloading file "/textfile0 (2).txt" with range "bytes=10-18"
+ Then Downloaded content should be "test text"
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | share_type | 0 |
+ | uid_owner | user1 |
+ | displayname_owner | user1 |
+ | path | /textfile0 (2).txt |
+ | item_type | file |
+ | mimetype | text/plain |
+ | storage_id | shared::/textfile0 (2).txt |
+ | file_target | /textfile0 (2).txt |
+ | share_with | user2 |
+ | share_with_displayname | user2 |
+
Scenario: getting all shares of a file with a received share also reshared after revoking the resharing rights
Given user "user0" exists
And user "user1" exists
diff --git a/build/integration/sharing_features/sharing-v1-part4.feature b/build/integration/sharing_features/sharing-v1-part4.feature
index 3b825aebd1813..5e710d7f28647 100644
--- a/build/integration/sharing_features/sharing-v1-part4.feature
+++ b/build/integration/sharing_features/sharing-v1-part4.feature
@@ -315,3 +315,114 @@ Scenario: Can copy file between shares if share permissions
And the OCS status code should be "100"
When User "user1" copies file "/share/test.txt" to "/re-share/movetest.txt"
Then the HTTP status code should be "201"
+
+Scenario: Group deletes removes mount without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And user "user0" belongs to group "group0"
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ And group "group0" does not exist
+ Then Share mounts for "user0" are empty
+
+Scenario: Group deletes removes mount with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And user "user0" belongs to group "group0"
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ And group "group0" does not exist
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
+
+Scenario: User share mount without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Deleting last share
+ Then Share mounts for "user0" are empty
+
+Scenario: User share mount with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Deleting last share
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
+
+Scenario: User added/removed to group share without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When user "user0" belongs to group "group0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When As an "admin"
+ Then sending "DELETE" to "/cloud/users/user0/groups" with
+ | groupid | group0 |
+ Then As an "user0"
+ And Share mounts for "user0" are empty
+
+Scenario: User added/removed to group share with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ When user "user0" belongs to group "group0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When As an "admin"
+ Then sending "DELETE" to "/cloud/users/user0/groups" with
+ | groupid | group0 |
+ Then As an "user0"
+ And Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
+
+ Scenario: Share moved without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When User "user0" moves file "/textfile0 (2).txt" to "/target.txt"
+ Then Share mounts for "user0" match
+ | /user0/files/target.txt/ |
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 44d9dd2c6a998..ca55f66b3cf5b 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -470,6 +470,7 @@
'OCP\\Files\\Events\\Node\\NodeRenamedEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeRenamedEvent.php',
'OCP\\Files\\Events\\Node\\NodeTouchedEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeTouchedEvent.php',
'OCP\\Files\\Events\\Node\\NodeWrittenEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeWrittenEvent.php',
+ 'OCP\\Files\\Events\\UserHomeSetupEvent' => $baseDir . '/lib/public/Files/Events/UserHomeSetupEvent.php',
'OCP\\Files\\File' => $baseDir . '/lib/public/Files/File.php',
'OCP\\Files\\FileInfo' => $baseDir . '/lib/public/Files/FileInfo.php',
'OCP\\Files\\FileNameTooLongException' => $baseDir . '/lib/public/Files/FileNameTooLongException.php',
@@ -849,6 +850,7 @@
'OCP\\Share\\Events\\ShareCreatedEvent' => $baseDir . '/lib/public/Share/Events/ShareCreatedEvent.php',
'OCP\\Share\\Events\\ShareDeletedEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
+ 'OCP\\Share\\Events\\ShareMovedEvent' => $baseDir . '/lib/public/Share/Events/ShareMovedEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => $baseDir . '/lib/public/Share/Events/ShareTransferredEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => $baseDir . '/lib/public/Share/Events/VerifyMountPointEvent.php',
'OCP\\Share\\Exceptions\\AlreadySharedException' => $baseDir . '/lib/public/Share/Exceptions/AlreadySharedException.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 36080f5fac4bc..0b4375c0a7f57 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -511,6 +511,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\Events\\Node\\NodeRenamedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeRenamedEvent.php',
'OCP\\Files\\Events\\Node\\NodeTouchedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeTouchedEvent.php',
'OCP\\Files\\Events\\Node\\NodeWrittenEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeWrittenEvent.php',
+ 'OCP\\Files\\Events\\UserHomeSetupEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/UserHomeSetupEvent.php',
'OCP\\Files\\File' => __DIR__ . '/../../..' . '/lib/public/Files/File.php',
'OCP\\Files\\FileInfo' => __DIR__ . '/../../..' . '/lib/public/Files/FileInfo.php',
'OCP\\Files\\FileNameTooLongException' => __DIR__ . '/../../..' . '/lib/public/Files/FileNameTooLongException.php',
@@ -890,6 +891,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Share\\Events\\ShareCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareCreatedEvent.php',
'OCP\\Share\\Events\\ShareDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
+ 'OCP\\Share\\Events\\ShareMovedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareMovedEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareTransferredEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/VerifyMountPointEvent.php',
'OCP\\Share\\Exceptions\\AlreadySharedException' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/AlreadySharedException.php',
diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php
index 65495434ad6b1..5e0653b1c63f6 100644
--- a/lib/private/Files/Config/UserMountCache.php
+++ b/lib/private/Files/Config/UserMountCache.php
@@ -5,8 +5,10 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
+
namespace OC\Files\Config;
+use OC\DB\Exceptions\DbalException;
use OC\User\LazyUser;
use OCP\Cache\CappedMemoryCache;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -32,11 +34,13 @@ class UserMountCache implements IUserMountCache {
/**
* Cached mount info.
+ *
* @var CappedMemoryCache
**/
private CappedMemoryCache $mountsForUsers;
/**
* fileid => internal path mapping for cached mount info.
+ *
* @var CappedMemoryCache
**/
private CappedMemoryCache $internalPathCache;
@@ -72,7 +76,9 @@ public function registerMounts(IUser $user, array $mounts, ?array $mountProvider
$cachedMounts = $this->getMountsForUser($user);
if (is_array($mountProviderClasses)) {
- $cachedMounts = array_filter($cachedMounts, function (ICachedMountInfo $mountInfo) use ($mountProviderClasses, $newMounts) {
+ $cachedMounts = array_filter($cachedMounts, function (
+ ICachedMountInfo $mountInfo,
+ ) use ($mountProviderClasses, $newMounts) {
// for existing mounts that didn't have a mount provider set
// we still want the ones that map to new mounts
if ($mountInfo->getMountProvider() === '' && isset($newMounts[$mountInfo->getKey()])) {
@@ -482,21 +488,13 @@ public function clear(): void {
}
public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
- $mounts = [];
- foreach ($this->getMountsForUser($user) as $mount) {
- $mounts[$mount->getMountPoint()] = $mount;
- }
-
+ $searchPaths = [];
$current = rtrim($path, '/');
- // walk up the directory tree until we find a path that has a mountpoint set
- // the loop will return if a mountpoint is found or break if none are found
- while (true) {
+ // get all paths that we are interested in, $path and all it's parents
+ while ($current !== '') {
$mountPoint = $current . '/';
- if (isset($mounts[$mountPoint])) {
- return $mounts[$mountPoint];
- } elseif ($current === '') {
- break;
- }
+
+ $searchPaths[] = $mountPoint;
$current = dirname($current);
if ($current === '.' || $current === '/') {
@@ -504,6 +502,34 @@ public function getMountForPath(IUser $user, string $path): ICachedMountInfo {
}
}
+ $mounts = [];
+ if (isset($this->mountsForUsers[$user->getUID()])) {
+ foreach ($this->mountsForUsers[$user->getUID()] as $mount) {
+ $mounts[$mount->getMountPoint()] = $mount;
+ }
+ } else {
+ $searchPathHashes = array_map(static fn (string $path) => hash('xxh128', $path), $searchPaths);
+
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())))
+ ->andWhere($builder->expr()->in('mount_point_hash', $builder->createNamedParameter($searchPathHashes, IQueryBuilder::PARAM_STR_ARRAY)));
+
+ foreach ($query->executeQuery()->fetchAll() as $row) {
+ $mount = $this->dbRowToMountInfo($row);
+ $mounts[$mount->getMountPoint()] = $mount;
+ }
+ }
+
+ // note that $searchPaths is sorted deepest path first
+ foreach ($searchPaths as $searchPath) {
+ if (isset($mounts[$searchPath])) {
+ return $mounts[$searchPath];
+ }
+ }
+
throw new NotFoundException('No cached mount for path ' . $path);
}
@@ -519,23 +545,49 @@ public function getMountsInPath(IUser $user, string $path): array {
return $result;
}
- public function removeMount(string $mountPoint): void {
+ public function removeMount(string $mountPoint, ?IUser $user = null): void {
$query = $this->connection->getQueryBuilder();
$query->delete('mounts')
->where($query->expr()->eq('mount_point_hash', $query->createNamedParameter(hash('xxh128', $mountPoint))));
+ if ($user) {
+ $query->andWhere($query->expr()->eq('user_id', $query->createNamedParameter($user->getUID())));
+ }
$query->executeStatement();
+
+ $parts = explode('/', $mountPoint);
+ if (count($parts) > 3) {
+ [, $userId] = $parts;
+ unset($this->mountsForUsers[$userId]);
+ }
}
- public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void {
- $this->connection->insertIgnoreConflict('mounts', [
- 'storage_id' => $rootCacheEntry->getStorageId(),
- 'root_id' => $rootCacheEntry->getId(),
- 'user_id' => $user->getUID(),
- 'mount_point' => $mountPoint,
- 'mount_point_hash' => hash('xxh128', $mountPoint),
- 'mount_id' => $mountId,
- 'mount_provider_class' => $mountProvider
- ]);
+ public function addMount(
+ IUser $user,
+ string $mountPoint,
+ ICacheEntry $rootCacheEntry,
+ string $mountProvider,
+ ?int $mountId = null,
+ ): void {
+ $query = $this->connection->getQueryBuilder();
+ $query->insert('mounts')
+ ->values([
+ 'storage_id' => $query->createNamedParameter($rootCacheEntry->getStorageId()),
+ 'root_id' => $query->createNamedParameter($rootCacheEntry->getId()),
+ 'user_id' => $query->createNamedParameter($user->getUID()),
+ 'mount_point' => $query->createNamedParameter($mountPoint),
+ 'mount_point_hash' => $query->createNamedParameter(hash('xxh128', $mountPoint)),
+ 'mount_id' => $query->createNamedParameter($mountId),
+ 'mount_provider_class' => $query->createNamedParameter($mountProvider)
+ ]);
+
+ try {
+ $query->executeStatement();
+ unset($this->mountsForUsers[$user->getUID()]);
+ } catch (DbalException $e) {
+ if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ throw $e;
+ }
+ }
}
/**
@@ -546,4 +598,26 @@ public function flush(): void {
$this->internalPathCache = new CappedMemoryCache();
$this->mountsForUsers = new CappedMemoryCache();
}
+
+ public function getMountAtPath(IUser $user, string $mountPoint): ?ICachedMountInfo {
+ if (isset($this->mountsForUsers[$user->getUID()])) {
+ foreach ($this->mountsForUsers[$user->getUID()] as $mount) {
+ if ($mount->getMountPoint() === $mountPoint) {
+ return $mount;
+ }
+ }
+ return null;
+ }
+
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select('storage_id', 'root_id', 'user_id', 'mount_point', 'mount_id', 'f.path', 'mount_provider_class')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f', $builder->expr()->eq('m.root_id', 'f.fileid'))
+ ->where($builder->expr()->eq('user_id', $builder->createNamedParameter($user->getUID())))
+ ->andWhere($builder->expr()->eq('mount_point_hash', $builder->createNamedParameter(hash('xxh128', $mountPoint))))
+ ->setMaxResults(1);
+
+ $row = $query->executeQuery()->fetch();
+ return $row ? $this->dbRowToMountInfo($row) : null;
+ }
}
diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php
index 2c0f412b735cd..81e56345e3ba0 100644
--- a/lib/private/Files/FileInfo.php
+++ b/lib/private/Files/FileInfo.php
@@ -7,8 +7,8 @@
*/
namespace OC\Files;
+use OC\Files\Cache\CacheEntry;
use OC\Files\Mount\HomeMountPoint;
-use OCA\Files_Sharing\External\Mount;
use OCA\Files_Sharing\ISharedMountPoint;
use OCP\Constants;
use OCP\Files\Cache\ICacheEntry;
@@ -209,8 +209,12 @@ public function getType() {
return $this->data['type'];
}
- public function getData() {
- return $this->data;
+ public function getData(): ICacheEntry {
+ if ($this->data instanceof ICacheEntry) {
+ return $this->data;
+ } else {
+ return new CacheEntry($this->data);
+ }
}
/**
diff --git a/lib/private/Files/Mount/Manager.php b/lib/private/Files/Mount/Manager.php
index 3d586ba4c5911..9a29752229d3e 100644
--- a/lib/private/Files/Mount/Manager.php
+++ b/lib/private/Files/Mount/Manager.php
@@ -59,11 +59,14 @@ public function removeMount(string $mountPoint): void {
}
public function moveMount(string $mountPoint, string $target): void {
- $this->mounts[$target] = $this->mounts[$mountPoint];
- unset($this->mounts[$mountPoint]);
- $this->pathCache->clear();
- $this->inPathCache->clear();
- $this->areMountsSorted = false;
+ if ($mountPoint !== $target) {
+ $this->mounts[$target] = $this->mounts[$mountPoint];
+ $this->mounts[$target]->setMountPoint($target);
+ unset($this->mounts[$mountPoint]);
+ $this->pathCache->clear();
+ $this->inPathCache->clear();
+ $this->areMountsSorted = false;
+ }
}
/**
diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php
index 3be4a31b8b5bf..1bc663f76b6ae 100644
--- a/lib/private/Files/Node/LazyFolder.php
+++ b/lib/private/Files/Node/LazyFolder.php
@@ -10,6 +10,7 @@
use OC\Files\Filesystem;
use OC\Files\Utils\PathHelper;
use OCP\Constants;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
@@ -570,6 +571,10 @@ public function getMetadata(): array {
return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args());
}
+ public function getData(): ICacheEntry {
+ return $this->__call(__FUNCTION__, func_get_args());
+ }
+
public function verifyPath($fileName, $readonly = false): void {
$this->__call(__FUNCTION__, func_get_args());
}
diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php
index 40bd0626542c7..ca042dacf91bd 100644
--- a/lib/private/Files/Node/Node.php
+++ b/lib/private/Files/Node/Node.php
@@ -14,6 +14,7 @@
use OCP\Constants;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
@@ -485,4 +486,8 @@ public function getParentId(): int {
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}
+
+ public function getData(): ICacheEntry {
+ return $this->fileInfo->getData();
+ }
}
diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php
index ce6e479f6522f..a8c46a20bd31a 100644
--- a/lib/private/Files/SetupManager.php
+++ b/lib/private/Files/SetupManager.php
@@ -42,6 +42,7 @@
use OCP\Files\Events\InvalidateMountCacheEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
+use OCP\Files\Events\UserHomeSetupEvent;
use OCP\Files\ISetupManager;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
@@ -336,6 +337,9 @@ private function oneTimeUserSetup(IUser $user) {
$this->eventLogger->end('fs:setup:user:home:scan');
}
$this->eventLogger->end('fs:setup:user:home');
+
+ $event = new UserHomeSetupEvent($user, $homeMount);
+ $this->eventDispatcher->dispatchTyped($event);
} else {
$this->mountManager->addMount(new MountPoint(
new NullStorage([]),
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php
index 9515a578da9e9..f518b7ddafb5d 100644
--- a/lib/private/Share20/Manager.php
+++ b/lib/private/Share20/Manager.php
@@ -51,6 +51,7 @@
use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\Events\ShareDeletedEvent;
use OCP\Share\Events\ShareDeletedFromSelfEvent;
+use OCP\Share\Events\ShareMovedEvent;
use OCP\Share\Exceptions\AlreadySharedException;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
@@ -1185,13 +1186,16 @@ public function moveShare(IShare $share, string $recipientId): IShare {
if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
}
+ $recipient = $this->userManager->get($recipientId);
+ if (!$recipient) {
+ throw new \InvalidArgumentException($this->l->t('Unknown share recipient'));
+ }
if ($share->getShareType() === IShare::TYPE_GROUP) {
$sharedWith = $this->groupManager->get($share->getSharedWith());
if (is_null($sharedWith)) {
throw new \InvalidArgumentException($this->l->t('Group "%s" does not exist', [$share->getSharedWith()]));
}
- $recipient = $this->userManager->get($recipientId);
if (!$sharedWith->inGroup($recipient)) {
throw new \InvalidArgumentException($this->l->t('Invalid share recipient'));
}
@@ -1200,7 +1204,11 @@ public function moveShare(IShare $share, string $recipientId): IShare {
[$providerId,] = $this->splitFullId($share->getFullId());
$provider = $this->factory->getProvider($providerId);
- return $provider->move($share, $recipientId);
+ $result = $provider->move($share, $recipientId);
+
+ $this->dispatchEvent(new ShareMovedEvent($share, $recipient), 'share moved');
+
+ return $result;
}
#[Override]
diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php
index 3c395caec4b6f..3dd57fb35653e 100644
--- a/lib/private/Share20/Share.php
+++ b/lib/private/Share20/Share.php
@@ -62,6 +62,8 @@ class Share implements IShare {
private ?int $parent = null;
/** @var string */
private $target;
+ /** @var string */
+ private ?string $originalTarget = null;
/** @var \DateTime */
private $shareTime;
/** @var bool */
@@ -514,10 +516,21 @@ public function getParent(): ?int {
* @inheritdoc
*/
public function setTarget($target) {
+ // if the target is changed, save the original target
+ if ($this->target && !$this->originalTarget) {
+ $this->originalTarget = $this->target;
+ }
$this->target = $target;
return $this;
}
+ /**
+ * Return the original target, if this share was moved
+ */
+ public function getOriginalTarget(): ?string {
+ return $this->originalTarget;
+ }
+
/**
* @inheritdoc
*/
diff --git a/lib/public/Files/Config/IUserMountCache.php b/lib/public/Files/Config/IUserMountCache.php
index 6e4ca41cd2816..fc0ec3f1f9284 100644
--- a/lib/public/Files/Config/IUserMountCache.php
+++ b/lib/public/Files/Config/IUserMountCache.php
@@ -113,7 +113,10 @@ public function getUsedSpaceForUsers(array $users);
public function clear(): void;
/**
- * Get all cached mounts for a user
+ * Get the cached mount for a path
+ *
+ * This walks up the directly tree until a mount is found, if you only want
+ * to get the mount at the specific path, use `getMountAtPath` instead.
*
* @param IUser $user
* @param string $path
@@ -139,7 +142,7 @@ public function getMountsInPath(IUser $user, string $path): array;
*
* @since 33.0.0
*/
- public function removeMount(string $mountPoint): void;
+ public function removeMount(string $mountPoint, ?IUser $user = null): void;
/**
* Register a new mountpoint for a user
@@ -147,4 +150,11 @@ public function removeMount(string $mountPoint): void;
* @since 33.0.0
*/
public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void;
+
+ /**
+ * Get the mount at the specified path, if any
+ *
+ * @since 33.0.2
+ */
+ public function getMountAtPath(IUser $user, string $mountPoint): ?ICachedMountInfo;
}
diff --git a/lib/public/Files/Events/UserHomeSetupEvent.php b/lib/public/Files/Events/UserHomeSetupEvent.php
new file mode 100644
index 0000000000000..2b49f64a28b92
--- /dev/null
+++ b/lib/public/Files/Events/UserHomeSetupEvent.php
@@ -0,0 +1,46 @@
+user;
+ }
+
+ /**
+ * @since 34.0.0
+ */
+ public function getHomeMount(): IMountPoint {
+ return $this->homeMount;
+ }
+}
diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php
index 942e4f51c31dd..76b2083085daf 100644
--- a/lib/public/Files/FileInfo.php
+++ b/lib/public/Files/FileInfo.php
@@ -8,6 +8,7 @@
namespace OCP\Files;
use OCP\AppFramework\Attribute\Consumable;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Storage\IStorage;
/**
@@ -308,4 +309,12 @@ public function getParentId(): int;
* @since 28.0.0
*/
public function getMetadata(): array;
+
+ /**
+ * Get the filecache data for the file
+ *
+ * @return ICacheEntry
+ * @since 34.0.0
+ */
+ public function getData(): ICacheEntry;
}
diff --git a/lib/public/Share/Events/ShareMovedEvent.php b/lib/public/Share/Events/ShareMovedEvent.php
new file mode 100644
index 0000000000000..3c9fc71f1933f
--- /dev/null
+++ b/lib/public/Share/Events/ShareMovedEvent.php
@@ -0,0 +1,42 @@
+share;
+ }
+
+ /**
+ * @since 33.0.0
+ */
+ public function getUser(): IUser {
+ return $this->user;
+ }
+}
diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php
index da8b9e4868ace..7bd1b8c5ebda3 100644
--- a/lib/public/Share/IShare.php
+++ b/lib/public/Share/IShare.php
@@ -545,6 +545,13 @@ public function getParent(): ?int;
*/
public function setTarget($target);
+ /**
+ * Return the original target, if this share was moved
+ *
+ * @since 33.0.0
+ */
+ public function getOriginalTarget(): ?string;
+
/**
* Get the target path of this share relative to the recipients user folder.
*
diff --git a/tests/lib/Files/Config/UserMountCacheTest.php b/tests/lib/Files/Config/UserMountCacheTest.php
index 6e6630b5be308..badb8f7c45b6b 100644
--- a/tests/lib/Files/Config/UserMountCacheTest.php
+++ b/tests/lib/Files/Config/UserMountCacheTest.php
@@ -183,7 +183,9 @@ public function testRemoveMounts(): void {
$this->eventDispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
- ->with($this->callback(function (UserMountAddedEvent|UserMountRemovedEvent $event) use (&$operation) {
+ ->with($this->callback(function (
+ UserMountAddedEvent|UserMountRemovedEvent $event,
+ ) use (&$operation) {
return match (++$operation) {
1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/asd/',
2 => $event instanceof UserMountRemovedEvent && $event->mountPoint->getMountPoint() === '/asd/',
@@ -214,7 +216,9 @@ public function testChangeMounts(): void {
$this->eventDispatcher
->expects($this->exactly(3))
->method('dispatchTyped')
- ->with($this->callback(function (UserMountAddedEvent|UserMountRemovedEvent $event) use (&$operation) {
+ ->with($this->callback(function (
+ UserMountAddedEvent|UserMountRemovedEvent $event,
+ ) use (&$operation) {
return match (++$operation) {
1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/bar/',
2 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/foo/',
@@ -250,7 +254,9 @@ public function testChangeMountId(): void {
$this->eventDispatcher
->expects($this->exactly(2))
->method('dispatchTyped')
- ->with($this->callback(function (UserMountAddedEvent|UserMountUpdatedEvent $event) use (&$operation) {
+ ->with($this->callback(function (
+ UserMountAddedEvent|UserMountUpdatedEvent $event,
+ ) use (&$operation) {
return match (++$operation) {
1 => $event instanceof UserMountAddedEvent && $event->mountPoint->getMountPoint() === '/foo/',
2 => $event instanceof UserMountUpdatedEvent && $event->oldMountPoint->getMountId() === null && $event->newMountPoint->getMountId() === 1,
@@ -611,12 +617,59 @@ public function testChangedSameRootId(): void {
$this->cache->flush();
$cached = $this->cache->getMountsForUser($user);
- usort($cached, fn (ICachedMountInfo $a, ICachedMountInfo $b) => $a->getMountPoint() <=> $b->getMountPoint());
+ usort($cached, fn (ICachedMountInfo $a, ICachedMountInfo $b,
+ ) => $a->getMountPoint() <=> $b->getMountPoint());
- $mountPoints = array_map(fn (ICachedMountInfo $mountInfo) => $mountInfo->getMountPoint(), $cached);
+ $mountPoints = array_map(fn (ICachedMountInfo $mountInfo,
+ ) => $mountInfo->getMountPoint(), $cached);
$this->assertEquals(['/asd/', '/asd2/'], $mountPoints);
- $mountIds = array_map(fn (ICachedMountInfo $mountInfo) => $mountInfo->getMountId(), $cached);
+ $mountIds = array_map(fn (ICachedMountInfo $mountInfo,
+ ) => $mountInfo->getMountId(), $cached);
$this->assertEquals([null, 1], $mountIds);
}
+
+ public function testGetMountForPath(): void {
+ $user = $this->userManager->get('u1');
+
+ [$storage] = $this->getStorage(10);
+ $mount1 = new MountPoint($storage, '/asd/');
+ $mount2 = new MountPoint($storage, '/asd/foo');
+
+ $this->cache->registerMounts($user, [$mount1, $mount2]);
+ $this->cache->flush();
+
+ $this->assertEquals('/asd/', $this->cache->getMountForPath($user, '/asd/bar/')->getMountPoint());
+ $this->assertEquals('/asd/', $this->cache->getMountForPath($user, '/asd/')->getMountPoint());
+ $this->assertEquals('/asd/foo/', $this->cache->getMountForPath($user, '/asd/foo/bar/')->getMountPoint());
+ $this->assertEquals('/asd/foo/', $this->cache->getMountForPath($user, '/asd/foo/')->getMountPoint());
+ }
+
+ public function testGetMountsInPath(): void {
+ $user = $this->userManager->get('u1');
+
+ [$storage] = $this->getStorage(10);
+ $mount1 = new MountPoint($storage, '/asd/');
+ $mount2 = new MountPoint($storage, '/asd/foo/');
+ $mount3 = new MountPoint($storage, '/asd/foo/bar/');
+
+ $this->cache->registerMounts($user, [$mount1, $mount2, $mount3]);
+ $this->cache->flush();
+
+ $getMountPaths = function (string $path) use ($user) {
+ $mountPoints = array_values(
+ array_map(
+ fn (ICachedMountInfo $mount) => $mount->getMountPoint(),
+ $this->cache->getMountsInPath($user, $path)
+ )
+ );
+ sort($mountPoints);
+ return $mountPoints;
+ };
+
+ $this->assertEquals(['/asd/foo/', '/asd/foo/bar/'], $getMountPaths('/asd/'));
+ $this->assertEquals([], $getMountPaths('/asd/foo/bar/'));
+ $this->assertEquals(['/asd/foo/bar/'], $getMountPaths('/asd/foo/'));
+ $this->assertEquals([], $getMountPaths('/asd/bar/'));
+ }
}
diff --git a/tests/lib/Mock/Config/MockAppConfig.php b/tests/lib/Mock/Config/MockAppConfig.php
new file mode 100644
index 0000000000000..f601bf6dbc570
--- /dev/null
+++ b/tests/lib/Mock/Config/MockAppConfig.php
@@ -0,0 +1,169 @@
+config[$app][$key]);
+ }
+
+ public function getValues($app, $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getFilteredValues($app): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getApps(): array {
+ return array_keys($this->config);
+ }
+
+ public function getKeys(string $app): array {
+ return array_keys($this->config[$app] ?? []);
+ }
+
+ public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isLazy(string $app, string $key): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueString(string $app, string $key, string $default = '', bool $lazy = false): string {
+ return (string)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueInt(string $app, string $key, int $default = 0, bool $lazy = false): int {
+ return (int)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
+ return (float)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
+ return (bool)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueArray(string $app, string $key, array $default = [], bool $lazy = false): array {
+ return ($this->config[$app] ?? [])[$key] ?? $default;
+ }
+
+ public function getValueType(string $app, string $key, ?bool $lazy = null): int {
+ throw new \Exception('not implemented');
+ }
+
+ public function setValueString(string $app, string $key, string $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueInt(string $app, string $key, int $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueFloat(string $app, string $key, float $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueBool(string $app, string $key, bool $value, bool $lazy = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueArray(string $app, string $key, array $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function updateSensitive(string $app, string $key, bool $sensitive): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateLazy(string $app, string $key, bool $lazy): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getDetails(string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function convertTypeToInt(string $type): int {
+ return match (strtolower($type)) {
+ 'mixed' => IAppConfig::VALUE_MIXED,
+ 'string' => IAppConfig::VALUE_STRING,
+ 'integer' => IAppConfig::VALUE_INT,
+ 'float' => IAppConfig::VALUE_FLOAT,
+ 'boolean' => IAppConfig::VALUE_BOOL,
+ 'array' => IAppConfig::VALUE_ARRAY,
+ default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
+ };
+ }
+
+ public function convertTypeToString(int $type): string {
+ $type &= ~self::VALUE_SENSITIVE;
+
+ return match ($type) {
+ IAppConfig::VALUE_MIXED => 'mixed',
+ IAppConfig::VALUE_STRING => 'string',
+ IAppConfig::VALUE_INT => 'integer',
+ IAppConfig::VALUE_FLOAT => 'float',
+ IAppConfig::VALUE_BOOL => 'boolean',
+ IAppConfig::VALUE_ARRAY => 'array',
+ default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
+ };
+ }
+
+ public function deleteKey(string $app, string $key): void {
+ if ($this->hasKey($app, $key)) {
+ unset($this->config[$app][$key]);
+ }
+ }
+
+ public function deleteApp(string $app): void {
+ if (isset($this->config[$app])) {
+ unset($this->config[$app]);
+ }
+ }
+
+ public function clearCache(bool $reload = false): void {
+ }
+
+ public function searchKeys(string $app, string $prefix = '', bool $lazy = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getKeyDetails(string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAppInstalledVersions(bool $onlyEnabled = false): array {
+ throw new \Exception('not implemented');
+ }
+}
diff --git a/tests/lib/Mock/Config/MockUserConfig.php b/tests/lib/Mock/Config/MockUserConfig.php
new file mode 100644
index 0000000000000..cc4619ef191ef
--- /dev/null
+++ b/tests/lib/Mock/Config/MockUserConfig.php
@@ -0,0 +1,209 @@
+config);
+ }
+
+ public function getApps(string $userId): array {
+ return array_keys($this->config[$userId] ?? []);
+ }
+
+ public function getKeys(string $userId, string $app): array {
+ if (isset($this->config[$userId][$app])) {
+ return array_keys($this->config[$userId][$app]);
+ } else {
+ return [];
+ }
+ }
+
+ public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ return isset($this->config[$userId][$app][$key]);
+ }
+
+ public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isLazy(string $userId, string $app, string $key): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValues(string $userId, string $app, string $prefix = '', bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAllValues(string $userId, bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValuesByUsers(string $app, string $key, ?ValueType $typedAs = null, ?array $userIds = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValues(string $app, string $key, array $values): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueBool(string $app, string $key, bool $value): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueString(string $userId, string $app, string $key, string $default = '', bool $lazy = false): string {
+ if (isset($this->config[$userId][$app])) {
+ return (string)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueInt(string $userId, string $app, string $key, int $default = 0, bool $lazy = false): int {
+ if (isset($this->config[$userId][$app])) {
+ return (int)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueFloat(string $userId, string $app, string $key, float $default = 0, bool $lazy = false): float {
+ if (isset($this->config[$userId][$app])) {
+ return (float)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueBool(string $userId, string $app, string $key, bool $default = false, bool $lazy = false): bool {
+ if (isset($this->config[$userId][$app])) {
+ return (bool)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueArray(string $userId, string $app, string $key, array $default = [], bool $lazy = false): array {
+ if (isset($this->config[$userId][$app])) {
+ return $this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int {
+ throw new \Exception('not implemented');
+ }
+
+ public function setValueString(string $userId, string $app, string $key, string $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueInt(string $userId, string $app, string $key, int $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueFloat(string $userId, string $app, string $key, float $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueBool(string $userId, string $app, string $key, bool $value, bool $lazy = false): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueArray(string $userId, string $app, string $key, array $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalIndexed(string $app, string $key, bool $indexed): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalLazy(string $app, string $key, bool $lazy): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function getDetails(string $userId, string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteUserConfig(string $userId, string $app, string $key): void {
+ unset($this->config[$userId][$app][$key]);
+ }
+
+ public function deleteKey(string $app, string $key): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteApp(string $app): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteAllUserConfig(string $userId): void {
+ unset($this->config[$userId]);
+ }
+
+ public function clearCache(string $userId, bool $reload = false): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function clearCacheAll(): void {
+ throw new \Exception('not implemented');
+ }
+}
diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php
index 4b8c4efb064e5..5600d827f4d02 100644
--- a/tests/lib/Share20/ManagerTest.php
+++ b/tests/lib/Share20/ManagerTest.php
@@ -4679,6 +4679,9 @@ public function testMoveShareUser(): void {
$share->setShareType(IShare::TYPE_USER)
->setId('42')
->setProviderId('foo');
+ $this->userManager->method('get')
+ ->with('recipient')
+ ->willReturn($this->createMock(IUser::class));
$share->setSharedWith('recipient');