Skip to content

Commit 84e2ebe

Browse files
authored
Merge pull request #59177 from nextcloud/revert-59172-revert-58894-stable33-authoritative-share
[stable33] authoritative share - revival
2 parents deb8ecc + 88134bc commit 84e2ebe

53 files changed

Lines changed: 2117 additions & 426 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/files/lib/Command/Mount/ListMounts.php

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88

99
namespace OCA\Files\Command\Mount;
1010

11+
use OC\Core\Command\Base;
1112
use OCP\Files\Config\ICachedMountInfo;
1213
use OCP\Files\Config\IMountProviderCollection;
1314
use OCP\Files\Config\IUserMountCache;
1415
use OCP\Files\Mount\IMountPoint;
1516
use OCP\IUserManager;
16-
use Symfony\Component\Console\Command\Command;
1717
use Symfony\Component\Console\Input\InputArgument;
1818
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
1920
use Symfony\Component\Console\Output\OutputInterface;
2021

21-
class ListMounts extends Command {
22+
class ListMounts extends Base {
2223
public function __construct(
2324
private readonly IUserManager $userManager,
2425
private readonly IUserMountCache $userMountCache,
@@ -28,52 +29,81 @@ public function __construct(
2829
}
2930

3031
protected function configure(): void {
32+
parent::configure();
3133
$this
3234
->setName('files:mount:list')
3335
->setDescription('List of mounts for a user')
34-
->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for');
36+
->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for')
37+
->addOption('cached-only', null, InputOption::VALUE_NONE, 'Only return cached mounts, prevents filesystem setup');
3538
}
3639

3740
public function execute(InputInterface $input, OutputInterface $output): int {
3841
$userId = $input->getArgument('user');
42+
$cachedOnly = $input->getOption('cached-only');
3943
$user = $this->userManager->get($userId);
4044
if (!$user) {
4145
$output->writeln("<error>User $userId not found</error>");
4246
return 1;
4347
}
4448

45-
$mounts = $this->mountProviderCollection->getMountsForUser($user);
46-
$mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
47-
/** @var array<string, IMountPoint> $cachedByMountpoint */
48-
$mountsByMountpoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
49+
if ($cachedOnly) {
50+
$mounts = [];
51+
} else {
52+
$mounts = $this->mountProviderCollection->getMountsForUser($user);
53+
$mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
54+
}
55+
/** @var array<string, IMountPoint> $cachedByMountPoint */
56+
$mountsByMountPoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
4957
usort($mounts, fn (IMountPoint $a, IMountPoint $b) => $a->getMountPoint() <=> $b->getMountPoint());
5058

5159
$cachedMounts = $this->userMountCache->getMountsForUser($user);
5260
usort($cachedMounts, fn (ICachedMountInfo $a, ICachedMountInfo $b) => $a->getMountPoint() <=> $b->getMountPoint());
5361
/** @var array<string, ICachedMountInfo> $cachedByMountpoint */
54-
$cachedByMountpoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
62+
$cachedByMountPoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
63+
64+
$format = $input->getOption('output');
5565

56-
foreach ($mounts as $mount) {
57-
$output->writeln('<info>' . $mount->getMountPoint() . '</info>: ' . $mount->getStorageId());
58-
if (isset($cachedByMountpoint[$mount->getMountPoint()])) {
59-
$cached = $cachedByMountpoint[$mount->getMountPoint()];
60-
$output->writeln("\t- provider: " . $cached->getMountProvider());
61-
$output->writeln("\t- storage id: " . $cached->getStorageId());
62-
$output->writeln("\t- root id: " . $cached->getRootId());
63-
} else {
64-
$output->writeln("\t<error>not registered</error>");
66+
if ($format === self::OUTPUT_FORMAT_PLAIN) {
67+
foreach ($mounts as $mount) {
68+
$output->writeln('<info>' . $mount->getMountPoint() . '</info>: ' . $mount->getStorageId());
69+
if (isset($cachedByMountPoint[$mount->getMountPoint()])) {
70+
$cached = $cachedByMountPoint[$mount->getMountPoint()];
71+
$output->writeln("\t- provider: " . $cached->getMountProvider());
72+
$output->writeln("\t- storage id: " . $cached->getStorageId());
73+
$output->writeln("\t- root id: " . $cached->getRootId());
74+
} else {
75+
$output->writeln("\t<error>not registered</error>");
76+
}
6577
}
66-
}
67-
foreach ($cachedMounts as $cachedMount) {
68-
if (!isset($mountsByMountpoint[$cachedMount->getMountPoint()])) {
69-
$output->writeln('<info>' . $cachedMount->getMountPoint() . '</info>:');
70-
$output->writeln("\t<error>registered but no longer provided</error>");
71-
$output->writeln("\t- provider: " . $cachedMount->getMountProvider());
72-
$output->writeln("\t- storage id: " . $cachedMount->getStorageId());
73-
$output->writeln("\t- root id: " . $cachedMount->getRootId());
78+
foreach ($cachedMounts as $cachedMount) {
79+
if ($cachedOnly || !isset($mountsByMountPoint[$cachedMount->getMountPoint()])) {
80+
$output->writeln('<info>' . $cachedMount->getMountPoint() . '</info>:');
81+
if (!$cachedOnly) {
82+
$output->writeln("\t<error>registered but no longer provided</error>");
83+
}
84+
$output->writeln("\t- provider: " . $cachedMount->getMountProvider());
85+
$output->writeln("\t- storage id: " . $cachedMount->getStorageId());
86+
$output->writeln("\t- root id: " . $cachedMount->getRootId());
87+
}
7488
}
89+
} else {
90+
$cached = array_map(fn (ICachedMountInfo $cachedMountInfo) => [
91+
'mountpoint' => $cachedMountInfo->getMountPoint(),
92+
'provider' => $cachedMountInfo->getMountProvider(),
93+
'storage_id' => $cachedMountInfo->getStorageId(),
94+
'root_id' => $cachedMountInfo->getRootId(),
95+
], $cachedMounts);
96+
$provided = array_map(fn (IMountPoint $cachedMountInfo) => [
97+
'mountpoint' => $cachedMountInfo->getMountPoint(),
98+
'provider' => $cachedMountInfo->getMountProvider(),
99+
'storage_id' => $cachedMountInfo->getStorageId(),
100+
'root_id' => $cachedMountInfo->getStorageRootId(),
101+
], $mounts);
102+
$this->writeArrayInOutputFormat($input, $output, array_filter([
103+
'cached' => $cached,
104+
'provided' => $cachedOnly ? null : $provided,
105+
]));
75106
}
76-
77107
return 0;
78108
}
79109

apps/files/lib/Service/OwnershipTransferService.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCA\Files_External\Config\ConfigAdapter;
2020
use OCA\GroupFolders\Mount\GroupMountPoint;
2121
use OCP\Encryption\IManager as IEncryptionManager;
22+
use OCP\EventDispatcher\IEventDispatcher;
2223
use OCP\Files\Config\IHomeMountProvider;
2324
use OCP\Files\Config\IUserMountCache;
2425
use OCP\Files\File;
@@ -31,6 +32,7 @@
3132
use OCP\IUserManager;
3233
use OCP\L10N\IFactory;
3334
use OCP\Server;
35+
use OCP\Share\Events\ShareTransferredEvent;
3436
use OCP\Share\IManager as IShareManager;
3537
use OCP\Share\IShare;
3638
use Symfony\Component\Console\Helper\ProgressBar;
@@ -53,6 +55,7 @@ public function __construct(
5355
private IUserManager $userManager,
5456
private IFactory $l10nFactory,
5557
private IRootFolder $rootFolder,
58+
private IEventDispatcher $eventDispatcher,
5659
) {
5760
}
5861

@@ -567,20 +570,23 @@ private function restoreShares(
567570
} catch (\Throwable $e) {
568571
$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
569572
}
573+
$this->eventDispatcher->dispatchTyped(new ShareTransferredEvent($share));
570574
$progress->advance();
571575
}
572576
$progress->finish();
573577
$output->writeln('');
574578
}
575579

576-
private function transferIncomingShares(string $sourceUid,
580+
private function transferIncomingShares(
581+
string $sourceUid,
577582
string $destinationUid,
578583
array $sourceShares,
579584
array $destinationShares,
580585
OutputInterface $output,
581586
string $path,
582587
string $finalTarget,
583-
bool $move): void {
588+
bool $move,
589+
): void {
584590
$output->writeln('Restoring incoming shares ...');
585591
$progress = new ProgressBar($output, count($sourceShares));
586592
$prefix = "$destinationUid/files";
@@ -619,8 +625,11 @@ private function transferIncomingShares(string $sourceUid,
619625
if ($move) {
620626
continue;
621627
}
628+
$oldMountPoint = $this->getShareMountPoint($destinationUid, $share->getTarget());
629+
$newMountPoint = $this->getShareMountPoint($destinationUid, $shareTarget);
622630
$share->setTarget($shareTarget);
623631
$this->shareManager->moveShare($share, $destinationUid);
632+
$this->mountManager->moveMount($oldMountPoint, $newMountPoint);
624633
continue;
625634
}
626635
$this->shareManager->deleteShare($share);
@@ -638,8 +647,11 @@ private function transferIncomingShares(string $sourceUid,
638647
if ($move) {
639648
continue;
640649
}
650+
$oldMountPoint = $this->getShareMountPoint($destinationUid, $share->getTarget());
651+
$newMountPoint = $this->getShareMountPoint($destinationUid, $shareTarget);
641652
$share->setTarget($shareTarget);
642653
$this->shareManager->moveShare($share, $destinationUid);
654+
$this->mountManager->moveMount($oldMountPoint, $newMountPoint);
643655
continue;
644656
}
645657
} catch (NotFoundException $e) {
@@ -652,4 +664,8 @@ private function transferIncomingShares(string $sourceUid,
652664
$progress->finish();
653665
$output->writeln('');
654666
}
667+
668+
private function getShareMountPoint(string $uid, string $target): string {
669+
return '/' . $uid . '/files/' . trim($target, '/') . '/';
670+
}
655671
}

apps/files_external/lib/Service/MountCacheService.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public function handle(Event $event): void {
7575

7676
public function handleDeletedStorage(StorageConfig $storage): void {
7777
foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) {
78-
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
78+
$this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
7979
}
8080
}
8181

@@ -87,7 +87,7 @@ public function handleAddedStorage(StorageConfig $storage): void {
8787

8888
public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void {
8989
foreach ($this->applicableHelper->diffApplicable($oldStorage, $newStorage) as $user) {
90-
$this->userMountCache->removeMount($oldStorage->getMountPointForUser($user));
90+
$this->userMountCache->removeMount($oldStorage->getMountPointForUser($user), $user);
9191
}
9292
foreach ($this->applicableHelper->diffApplicable($newStorage, $oldStorage) as $user) {
9393
$this->registerForUser($user, $newStorage);
@@ -156,7 +156,7 @@ private function handleUserRemoved(IGroup $group, IUser $user): void {
156156
$storages = $this->storagesService->getAllStoragesForGroup($group);
157157
foreach ($storages as $storage) {
158158
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
159-
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
159+
$this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
160160
}
161161
}
162162
}
@@ -181,7 +181,7 @@ private function handleGroupDeleted(IGroup $group): void {
181181
private function removeGroupFromStorage(StorageConfig $storage, IGroup $group): void {
182182
foreach ($group->searchUsers('') as $user) {
183183
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
184-
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
184+
$this->userMountCache->removeMount($storage->getMountPointForUser($user), $user);
185185
}
186186
}
187187
}

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@
7070
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
7171
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
7272
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
73+
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
7374
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
75+
'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => $baseDir . '/../lib/Listener/UserHomeSetupListener.php',
7476
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php',
7577
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php',
7678
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => $baseDir . '/../lib/Middleware/ShareInfoMiddleware.php',
@@ -97,6 +99,7 @@
9799
'OCA\\Files_Sharing\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
98100
'OCA\\Files_Sharing\\ShareBackend\\File' => $baseDir . '/../lib/ShareBackend/File.php',
99101
'OCA\\Files_Sharing\\ShareBackend\\Folder' => $baseDir . '/../lib/ShareBackend/Folder.php',
102+
'OCA\\Files_Sharing\\ShareRecipientUpdater' => $baseDir . '/../lib/ShareRecipientUpdater.php',
100103
'OCA\\Files_Sharing\\ShareTargetValidator' => $baseDir . '/../lib/ShareTargetValidator.php',
101104
'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php',
102105
'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ class ComposerStaticInitFiles_Sharing
8585
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
8686
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
8787
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
88+
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
8889
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
90+
'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => __DIR__ . '/..' . '/../lib/Listener/UserHomeSetupListener.php',
8991
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php',
9092
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php',
9193
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ShareInfoMiddleware.php',
@@ -112,6 +114,7 @@ class ComposerStaticInitFiles_Sharing
112114
'OCA\\Files_Sharing\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
113115
'OCA\\Files_Sharing\\ShareBackend\\File' => __DIR__ . '/..' . '/../lib/ShareBackend/File.php',
114116
'OCA\\Files_Sharing\\ShareBackend\\Folder' => __DIR__ . '/..' . '/../lib/ShareBackend/Folder.php',
117+
'OCA\\Files_Sharing\\ShareRecipientUpdater' => __DIR__ . '/..' . '/../lib/ShareRecipientUpdater.php',
115118
'OCA\\Files_Sharing\\ShareTargetValidator' => __DIR__ . '/..' . '/../lib/ShareTargetValidator.php',
116119
'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php',
117120
'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php',

apps/files_sharing/lib/AppInfo/Application.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\Files\Event\LoadSidebar;
1515
use OCA\Files_Sharing\Capabilities;
1616
use OCA\Files_Sharing\Config\ConfigLexicon;
17+
use OCA\Files_Sharing\Event\UserShareAccessUpdatedEvent;
1718
use OCA\Files_Sharing\External\Manager;
1819
use OCA\Files_Sharing\External\MountProvider as ExternalMountProvider;
1920
use OCA\Files_Sharing\Helper;
@@ -24,7 +25,9 @@
2425
use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener;
2526
use OCA\Files_Sharing\Listener\LoadSidebarListener;
2627
use OCA\Files_Sharing\Listener\ShareInteractionListener;
28+
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
2729
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
30+
use OCA\Files_Sharing\Listener\UserHomeSetupListener;
2831
use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
2932
use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
3033
use OCA\Files_Sharing\Middleware\ShareInfoMiddleware;
@@ -46,13 +49,19 @@
4649
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
4750
use OCP\Files\Events\BeforeZipCreatedEvent;
4851
use OCP\Files\Events\Node\BeforeNodeReadEvent;
52+
use OCP\Files\Events\UserHomeSetupEvent;
53+
use OCP\Group\Events\BeforeGroupDeletedEvent;
4954
use OCP\Group\Events\GroupChangedEvent;
5055
use OCP\Group\Events\GroupDeletedEvent;
5156
use OCP\Group\Events\UserAddedEvent;
57+
use OCP\Group\Events\UserRemovedEvent;
5258
use OCP\IConfig;
5359
use OCP\IDBConnection;
5460
use OCP\IGroup;
61+
use OCP\Share\Events\BeforeShareDeletedEvent;
5562
use OCP\Share\Events\ShareCreatedEvent;
63+
use OCP\Share\Events\ShareMovedEvent;
64+
use OCP\Share\Events\ShareTransferredEvent;
5665
use OCP\User\Events\UserChangedEvent;
5766
use OCP\User\Events\UserDeletedEvent;
5867
use OCP\Util;
@@ -111,6 +120,18 @@ function () use ($c) {
111120
// File request auth
112121
$context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class);
113122

123+
// Update mounts
124+
$context->registerEventListener(ShareCreatedEvent::class, SharesUpdatedListener::class);
125+
$context->registerEventListener(BeforeShareDeletedEvent::class, SharesUpdatedListener::class);
126+
$context->registerEventListener(ShareTransferredEvent::class, SharesUpdatedListener::class);
127+
$context->registerEventListener(UserAddedEvent::class, SharesUpdatedListener::class);
128+
$context->registerEventListener(UserRemovedEvent::class, SharesUpdatedListener::class);
129+
$context->registerEventListener(BeforeGroupDeletedEvent::class, SharesUpdatedListener::class);
130+
$context->registerEventListener(GroupDeletedEvent::class, SharesUpdatedListener::class);
131+
$context->registerEventListener(UserShareAccessUpdatedEvent::class, SharesUpdatedListener::class);
132+
$context->registerEventListener(ShareMovedEvent::class, SharesUpdatedListener::class);
133+
$context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);
134+
114135
$context->registerConfigLexicon(ConfigLexicon::class);
115136
}
116137

apps/files_sharing/lib/Config/ConfigLexicon.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class ConfigLexicon implements ILexicon {
2424
public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal';
2525
public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal';
2626
public const EXCLUDE_RESHARE_FROM_EDIT = 'shareapi_exclude_reshare_from_edit';
27+
public const UPDATE_CUTOFF_TIME = 'update_cutoff_time';
28+
public const USER_NEEDS_SHARE_REFRESH = 'user_needs_share_refresh';
2729

2830
public function getStrictness(): Strictness {
2931
return Strictness::IGNORE;
@@ -34,10 +36,14 @@ public function getAppConfigs(): array {
3436
new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true),
3537
new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true),
3638
new Entry(self::EXCLUDE_RESHARE_FROM_EDIT, ValueType::BOOL, false, 'Exclude reshare permission from "Allow editing" bundled permissions'),
39+
40+
new Entry(self::UPDATE_CUTOFF_TIME, ValueType::FLOAT, 3.0, 'For how how long do we update the share data immediately before switching to only marking the user'),
3741
];
3842
}
3943

4044
public function getUserConfigs(): array {
41-
return [];
45+
return [
46+
new Entry(self::USER_NEEDS_SHARE_REFRESH, ValueType::BOOL, true, 'whether a user needs to have the receiving share data refreshed for possible changes'),
47+
];
4248
}
4349
}

0 commit comments

Comments
 (0)