mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 01:30:50 -04:00
Merge pull request #57760 from nextcloud/share-update-check-single
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (master, main, 8.4, main, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, guests_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / changes (push) Has been cancelled
Psalm static code analysis / static-code-analysis (push) Has been cancelled
Psalm static code analysis / static-code-analysis-security (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ocp (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ncu (push) Has been cancelled
Psalm static code analysis / static-code-analysis-strict (push) Has been cancelled
Psalm static code analysis / static-code-analysis-summary (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (master, main, 8.4, main, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, guests_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, main, 8.4, main, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / changes (push) Has been cancelled
Psalm static code analysis / static-code-analysis (push) Has been cancelled
Psalm static code analysis / static-code-analysis-security (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ocp (push) Has been cancelled
Psalm static code analysis / static-code-analysis-ncu (push) Has been cancelled
Psalm static code analysis / static-code-analysis-strict (push) Has been cancelled
Psalm static code analysis / static-code-analysis-summary (push) Has been cancelled
Active share validation/authoritative mount improvements
This commit is contained in:
commit
d80a3a7959
42 changed files with 1774 additions and 163 deletions
|
|
@ -8,17 +8,18 @@ declare(strict_types=1);
|
|||
|
||||
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 @@ class ListMounts extends Command {
|
|||
}
|
||||
|
||||
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("<error>User $userId not found</error>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$mounts = $this->mountProviderCollection->getMountsForUser($user);
|
||||
$mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
|
||||
/** @var array<string, IMountPoint> $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<string, IMountPoint> $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<string, ICachedMountInfo> $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);
|
||||
|
||||
foreach ($mounts as $mount) {
|
||||
$output->writeln('<info>' . $mount->getMountPoint() . '</info>: ' . $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("\t<error>not registered</error>");
|
||||
}
|
||||
}
|
||||
foreach ($cachedMounts as $cachedMount) {
|
||||
if (!isset($mountsByMountpoint[$cachedMount->getMountPoint()])) {
|
||||
$output->writeln('<info>' . $cachedMount->getMountPoint() . '</info>:');
|
||||
$output->writeln("\t<error>registered but no longer provided</error>");
|
||||
$output->writeln("\t- provider: " . $cachedMount->getMountProvider());
|
||||
$output->writeln("\t- storage id: " . $cachedMount->getStorageId());
|
||||
$output->writeln("\t- root id: " . $cachedMount->getRootId());
|
||||
}
|
||||
}
|
||||
$format = $input->getOption('output');
|
||||
|
||||
if ($format === self::OUTPUT_FORMAT_PLAIN) {
|
||||
foreach ($mounts as $mount) {
|
||||
$output->writeln('<info>' . $mount->getMountPoint() . '</info>: ' . $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("\t<error>not registered</error>");
|
||||
}
|
||||
}
|
||||
foreach ($cachedMounts as $cachedMount) {
|
||||
if ($cachedOnly || !isset($mountsByMountPoint[$cachedMount->getMountPoint()])) {
|
||||
$output->writeln('<info>' . $cachedMount->getMountPoint() . '</info>:');
|
||||
if (!$cachedOnly) {
|
||||
$output->writeln("\t<error>registered but no longer provided</error>");
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -577,14 +577,16 @@ class OwnershipTransferService {
|
|||
$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 @@ class OwnershipTransferService {
|
|||
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 @@ class OwnershipTransferService {
|
|||
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 @@ class OwnershipTransferService {
|
|||
$progress->finish();
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
private function getShareMountPoint(string $uid, string $target): string {
|
||||
return '/' . $uid . '/files/' . trim($target, '/') . '/';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class MountCacheService implements IEventListener {
|
|||
|
||||
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 @@ class MountCacheService implements IEventListener {
|
|||
|
||||
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 @@ class MountCacheService implements IEventListener {
|
|||
$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 @@ class MountCacheService implements IEventListener {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ return array(
|
|||
'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 @@ return array(
|
|||
'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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use OCA\Files_Sharing\Listener\LoadSidebarListener;
|
|||
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\Config\IMountProviderCollection;
|
|||
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\IDBConnection;
|
|||
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 @@ class Application extends App implements IBootstrap {
|
|||
$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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ class ConfigLexicon implements ILexicon {
|
|||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,93 +8,149 @@ declare(strict_types=1);
|
|||
|
||||
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<UserAddedEvent|UserRemovedEvent|ShareCreatedEvent|ShareTransferredEvent|BeforeShareDeletedEvent|UserShareAccessUpdatedEvent>
|
||||
* @psalm-type GroupEvents = UserAddedEvent|UserRemovedEvent|GroupDeletedEvent|BeforeGroupDeletedEvent
|
||||
* @template-implements IEventListener<GroupEvents|ShareCreatedEvent|ShareTransferredEvent|BeforeShareDeletedEvent|UserShareAccessUpdatedEvent|ShareMovedEvent>
|
||||
*/
|
||||
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 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 GroupDeletedEvent) {
|
||||
// so we can iterate them after the group is deleted
|
||||
foreach ($event->getGroup()->getUsers() as $user) {
|
||||
$this->updateOrMarkUser($user);
|
||||
}
|
||||
}
|
||||
if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
|
||||
$this->updateForUser($event->getUser(), true);
|
||||
$this->updateOrMarkUser($event->getUser());
|
||||
}
|
||||
if (
|
||||
$event instanceof ShareCreatedEvent
|
||||
|| $event instanceof ShareTransferredEvent
|
||||
) {
|
||||
foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
|
||||
$this->updateForUser($user, true);
|
||||
}
|
||||
}
|
||||
if ($event instanceof BeforeShareDeletedEvent) {
|
||||
foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
|
||||
$this->updateForUser($user, false, [$event->getShare()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private function updateForUser(IUser $user, bool $verifyMountPoints, array $ignoreShares = []): void {
|
||||
// prevent recursion
|
||||
if (isset($this->inUpdate[$user->getUID()])) {
|
||||
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, $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 ($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();
|
||||
|
||||
if ($mountsChanged) {
|
||||
$newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
|
||||
$this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unset($this->inUpdate[$user->getUID()]);
|
||||
$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);
|
||||
}
|
||||
|
||||
public function setCutOffMarkTime(float|int $cutOffMarkTime): void {
|
||||
$this->cutOffMarkTime = (float)$cutOffMarkTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
apps/files_sharing/lib/Listener/UserHomeSetupListener.php
Normal file
53
apps/files_sharing/lib/Listener/UserHomeSetupListener.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Sharing\Listener;
|
||||
|
||||
use OCA\Files_Sharing\AppInfo\Application;
|
||||
use OCA\Files_Sharing\Config\ConfigLexicon;
|
||||
use OCA\Files_Sharing\ShareRecipientUpdater;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\UserHomeSetupEvent;
|
||||
|
||||
/**
|
||||
* Listen to the users filesystem setup being started, to perform any receiving share
|
||||
* work that was postponed.
|
||||
*
|
||||
* @template-implements IEventListener<UserHomeSetupEvent>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ class CleanupShareTarget implements IRepairStep {
|
|||
(int)$shareInfo['file_source'],
|
||||
$absoluteNewTarget,
|
||||
$targetParentNode->getMountPoint(),
|
||||
$userMounts,
|
||||
fn ($path) => $userMounts[$path] ?? null,
|
||||
);
|
||||
$newTarget = $userFolder->getRelativePath($absoluteNewTarget);
|
||||
|
||||
|
|
|
|||
113
apps/files_sharing/lib/ShareRecipientUpdater.php
Normal file
113
apps/files_sharing/lib/ShareRecipientUpdater.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Sharing;
|
||||
|
||||
use OCP\Files\Config\ICachedMountInfo;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
class ShareRecipientUpdater {
|
||||
private array $inUpdate = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly IUserMountCache $userMountCache,
|
||||
private readonly MountProvider $shareMountProvider,
|
||||
private readonly ShareTargetValidator $shareTargetValidator,
|
||||
private readonly IStorageFactory $storageFactory,
|
||||
private readonly IManager $shareManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all received shares for a user
|
||||
*/
|
||||
public function updateForUser(IUser $user): void {
|
||||
// prevent recursion
|
||||
if ($this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,7 @@ use OC\Files\View;
|
|||
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 @@ class ShareTargetValidator {
|
|||
/**
|
||||
* check if the parent folder exists otherwise move the mount point up
|
||||
*
|
||||
* @param array<string, ICachedMountInfo> $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 @@ class ShareTargetValidator {
|
|||
|
||||
/** @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 @@ class ShareTargetValidator {
|
|||
$share->getNodeId(),
|
||||
Filesystem::normalizePath($absoluteParent . '/' . $mountPoint),
|
||||
$parentMount,
|
||||
$allCachedMounts,
|
||||
$getMountByPath,
|
||||
);
|
||||
|
||||
/** @psalm-suppress InternalMethod */
|
||||
|
|
@ -112,13 +112,13 @@ class ShareTargetValidator {
|
|||
|
||||
|
||||
/**
|
||||
* @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 @@ class ShareTargetValidator {
|
|||
$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 @@ class ShareTargetValidator {
|
|||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Sharing\Tests\Listener;
|
||||
|
||||
use OCA\Files_Sharing\AppInfo\Application;
|
||||
use OCA\Files_Sharing\Config\ConfigLexicon;
|
||||
use OCA\Files_Sharing\Listener\UserHomeSetupListener;
|
||||
use OCA\Files_Sharing\ShareRecipientUpdater;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\Files\Events\UserHomeSetupEvent;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\IUser;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\Mock\Config\MockUserConfig;
|
||||
use Test\TestCase;
|
||||
|
||||
class UserHomeSetupListenerTest extends TestCase {
|
||||
private ShareRecipientUpdater&MockObject $updater;
|
||||
private IUserConfig $userConfig;
|
||||
private UserHomeSetupListener $listener;
|
||||
private IUser $user;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
216
apps/files_sharing/tests/ShareRecipientUpdaterTest.php
Normal file
216
apps/files_sharing/tests/ShareRecipientUpdaterTest.php
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Sharing\Tests;
|
||||
|
||||
use OCA\Files_Sharing\MountProvider;
|
||||
use OCA\Files_Sharing\ShareRecipientUpdater;
|
||||
use OCA\Files_Sharing\ShareTargetValidator;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\Config\ICachedMountInfo;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\Storage\IStorageFactory;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\Traits\UserTrait;
|
||||
|
||||
class ShareRecipientUpdaterTest extends \Test\TestCase {
|
||||
use UserTrait;
|
||||
|
||||
private IUserMountCache&MockObject $userMountCache;
|
||||
private MountProvider&MockObject $shareMountProvider;
|
||||
private ShareTargetValidator&MockObject $shareTargetValidator;
|
||||
private IStorageFactory&MockObject $storageFactory;
|
||||
private ShareRecipientUpdater $updater;
|
||||
private IManager $shareManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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<array{fileid: int, mount_point: string, provider: string}> $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,11 @@ declare(strict_types=1);
|
|||
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 @@ class ShareTargetValidatorTest extends TestCase {
|
|||
$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 @@ class ShareTargetValidatorTest extends TestCase {
|
|||
$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 @@ class ShareTargetValidatorTest extends TestCase {
|
|||
|
||||
$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 @@ class ShareTargetValidatorTest extends TestCase {
|
|||
$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 @@ class ShareTargetValidatorTest extends TestCase {
|
|||
$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());
|
||||
|
|
|
|||
191
apps/files_sharing/tests/SharesUpdatedListenerTest.php
Normal file
191
apps/files_sharing/tests/SharesUpdatedListenerTest.php
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Sharing\Tests;
|
||||
|
||||
use OCA\Files_Sharing\Config\ConfigLexicon;
|
||||
use OCA\Files_Sharing\Event\UserShareAccessUpdatedEvent;
|
||||
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
|
||||
use OCA\Files_Sharing\Listener\UserHomeSetupListener;
|
||||
use OCA\Files_Sharing\ShareRecipientUpdater;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\Events\BeforeShareDeletedEvent;
|
||||
use OCP\Share\Events\ShareCreatedEvent;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShare;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\Mock\Config\MockAppConfig;
|
||||
use Test\Mock\Config\MockUserConfig;
|
||||
use Test\Traits\UserTrait;
|
||||
|
||||
class SharesUpdatedListenerTest extends \Test\TestCase {
|
||||
use UserTrait;
|
||||
|
||||
private SharesUpdatedListener $sharesUpdatedListener;
|
||||
private ShareRecipientUpdater&MockObject $shareRecipientUpdater;
|
||||
private IManager&MockObject $manager;
|
||||
private IUserConfig $userConfig;
|
||||
private IAppConfig $appConfig;
|
||||
private ClockInterface&MockObject $clock;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private $clockFn;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ use OC\SystemConfig;
|
|||
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 @@ abstract class TestCase extends \Test\TestCase {
|
|||
$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 {
|
||||
|
|
|
|||
|
|
@ -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 @@ class TrashItem implements ITrashItem {
|
|||
public function getMetadata(): array {
|
||||
return $this->fileInfo->getMetadata();
|
||||
}
|
||||
|
||||
public function getData(): ICacheEntry {
|
||||
return $this->fileInfo->getData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ use Psr\Http\Message\ResponseInterface;
|
|||
require __DIR__ . '/autoload.php';
|
||||
|
||||
|
||||
|
||||
trait Sharing {
|
||||
use Provisioning;
|
||||
|
||||
|
|
@ -575,17 +575,17 @@ trait Sharing {
|
|||
$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 @@ trait Sharing {
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class SharingContext implements Context, SnippetAcceptingContext {
|
|||
$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']);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1011,7 +1011,7 @@ trait WebDav {
|
|||
*/
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/ |
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@ return array(
|
|||
'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 @@ return array(
|
|||
'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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<ICachedMountInfo[]>
|
||||
**/
|
||||
private CappedMemoryCache $mountsForUsers;
|
||||
/**
|
||||
* fileid => internal path mapping for cached mount info.
|
||||
*
|
||||
* @var CappedMemoryCache<string>
|
||||
**/
|
||||
private CappedMemoryCache $internalPathCache;
|
||||
|
|
@ -72,7 +76,9 @@ class UserMountCache implements IUserMountCache {
|
|||
|
||||
$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 @@ class UserMountCache implements IUserMountCache {
|
|||
}
|
||||
|
||||
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 @@ class UserMountCache implements IUserMountCache {
|
|||
}
|
||||
}
|
||||
|
||||
$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 @@ class UserMountCache implements IUserMountCache {
|
|||
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 @@ class UserMountCache implements IUserMountCache {
|
|||
$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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -59,11 +59,14 @@ class Manager implements IMountManager {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace OC\Files\Node;
|
|||
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 @@ class LazyFolder implements Folder {
|
|||
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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use OC\Files\View;
|
|||
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 @@ class Node implements INode {
|
|||
public function getMetadata(): array {
|
||||
return $this->fileInfo->getMetadata();
|
||||
}
|
||||
|
||||
public function getData(): ICacheEntry {
|
||||
return $this->fileInfo->getData();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ use OCP\Files\Events\BeforeFileSystemSetupEvent;
|
|||
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 @@ class SetupManager implements ISetupManager {
|
|||
$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([]),
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ use OCP\Share\Events\ShareAcceptedEvent;
|
|||
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 @@ class Manager implements IManager {
|
|||
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 @@ class Manager implements IManager {
|
|||
[$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]
|
||||
|
|
|
|||
|
|
@ -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 @@ class Share implements IShare {
|
|||
* @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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -113,7 +113,10 @@ interface IUserMountCache {
|
|||
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 @@ interface IUserMountCache {
|
|||
*
|
||||
* @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 @@ interface IUserMountCache {
|
|||
* @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;
|
||||
}
|
||||
|
|
|
|||
46
lib/public/Files/Events/UserHomeSetupEvent.php
Normal file
46
lib/public/Files/Events/UserHomeSetupEvent.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCP\Files\Events;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\IUser;
|
||||
|
||||
/**
|
||||
* Event triggered after the users home mount has been setup, before any other
|
||||
* mounts are setup.
|
||||
*
|
||||
* @since 34.0.0
|
||||
*/
|
||||
class UserHomeSetupEvent extends Event {
|
||||
/**
|
||||
* @since 34.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly IUser $user,
|
||||
private readonly IMountPoint $homeMount,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 34.0.0
|
||||
*/
|
||||
public function getUser(): IUser {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 34.0.0
|
||||
*/
|
||||
public function getHomeMount(): IMountPoint {
|
||||
return $this->homeMount;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 @@ interface FileInfo {
|
|||
* @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;
|
||||
}
|
||||
|
|
|
|||
42
lib/public/Share/Events/ShareMovedEvent.php
Normal file
42
lib/public/Share/Events/ShareMovedEvent.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCP\Share\Events;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\IShare;
|
||||
|
||||
/**
|
||||
* @since 33.0.0
|
||||
*/
|
||||
class ShareMovedEvent extends Event {
|
||||
/**
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly IShare $share,
|
||||
private readonly IUser $user,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public function getShare(): IShare {
|
||||
return $this->share;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public function getUser(): IUser {
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
|
|
@ -545,6 +545,13 @@ interface IShare {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -183,7 +183,9 @@ class UserMountCacheTest extends TestCase {
|
|||
$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 @@ class UserMountCacheTest extends TestCase {
|
|||
$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 @@ class UserMountCacheTest extends TestCase {
|
|||
$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 @@ class UserMountCacheTest extends TestCase {
|
|||
|
||||
$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/'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
169
tests/lib/Mock/Config/MockAppConfig.php
Normal file
169
tests/lib/Mock/Config/MockAppConfig.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Test\Mock\Config;
|
||||
|
||||
use OCP\Exceptions\AppConfigIncorrectTypeException;
|
||||
use OCP\IAppConfig;
|
||||
|
||||
class MockAppConfig implements IAppConfig {
|
||||
public function __construct(
|
||||
public array $config = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function hasKey(string $app, string $key, ?bool $lazy = false): bool {
|
||||
return isset($this->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');
|
||||
}
|
||||
}
|
||||
209
tests/lib/Mock/Config/MockUserConfig.php
Normal file
209
tests/lib/Mock/Config/MockUserConfig.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Test\Mock\Config;
|
||||
|
||||
use Generator;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\Config\ValueType;
|
||||
|
||||
class MockUserConfig implements IUserConfig {
|
||||
public function __construct(
|
||||
public array $config = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUserIds(string $appId = ''): array {
|
||||
return array_keys($this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -4679,6 +4679,9 @@ class ManagerTest extends \Test\TestCase {
|
|||
$share->setShareType(IShare::TYPE_USER)
|
||||
->setId('42')
|
||||
->setProviderId('foo');
|
||||
$this->userManager->method('get')
|
||||
->with('recipient')
|
||||
->willReturn($this->createMock(IUser::class));
|
||||
|
||||
$share->setSharedWith('recipient');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue