Skip to content

Commit 1dbdfe8

Browse files
committed
Enhancement: Make tables import asynchronous
Signed-off-by: Kostiantyn Miakshyn <[email protected]>
1 parent 318cdae commit 1dbdfe8

17 files changed

+483
-93
lines changed

appinfo/info.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ Have a good time and manage whatever you want.
5050
<database>sqlite</database>
5151
<nextcloud min-version="29" max-version="32"/>
5252
</dependencies>
53+
<activity>
54+
<settings>
55+
<setting>OCA\Tables\Activity\Setting</setting>
56+
</settings>
57+
<filters>
58+
<filter>OCA\Tables\Activity\Filter</filter>
59+
</filters>
60+
<providers>
61+
<provider>OCA\Tables\Activity\Provider</provider>
62+
</providers>
63+
</activity>
5364
<repair-steps>
5465
<pre-migration>
5566
<step>OCA\Tables\Migration\FixContextsDefaults</step>

lib/Activity/ActivityConstants.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Tables\Activity;
9+
10+
class ActivityConstants {
11+
public const APP_ID = 'tables';
12+
13+
/*****
14+
* Types can have different Settings for Mail/Notifications.
15+
*/
16+
public const TYPE_IMPORT_FINISHED = 'tables_import_finished';
17+
18+
/*****
19+
* Subjects are internal 'types', that get interpreted by our own Provider.
20+
*/
21+
22+
/**
23+
* Somebody shared a form to a selected user
24+
* Needs Params:
25+
* "user": The userId of the user who shared.
26+
* "formTitle": The hash of the shared form.
27+
* "formHash": The hash of the shared form
28+
*/
29+
public const SUBJECT_IMPORT_FINISHED = 'import_finished_subject';
30+
31+
public const MESSAGE_IMPORT_FINISHED = 'import_finished_message';
32+
}

lib/Activity/ActivityManager.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Tables\Activity;
9+
10+
use OCA\Tables\Model\ImportStats;
11+
use OCP\Activity\IManager;
12+
13+
class ActivityManager {
14+
public function __construct(
15+
protected IManager $activityManager,
16+
) {
17+
}
18+
19+
public function notifyImportFinished(string $userId, int $tableId, ImportStats $importStats): void {
20+
21+
$activity = $this->activityManager->generateEvent();
22+
$activity->setApp(ActivityConstants::APP_ID)
23+
->setType(ActivityConstants::TYPE_IMPORT_FINISHED)
24+
->setAuthor($userId)
25+
->setObject('table', $tableId)
26+
->setAffectedUser($userId)
27+
->setSubject(ActivityConstants::SUBJECT_IMPORT_FINISHED, [
28+
'actor' => $userId,
29+
'tableId' => $tableId,
30+
])
31+
->setMessage(ActivityConstants::MESSAGE_IMPORT_FINISHED, [
32+
'actor' => $userId,
33+
'tableId' => $tableId,
34+
] + (array)$importStats);
35+
36+
$this->activityManager->publish($activity);
37+
}
38+
}

lib/Activity/Filter.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Tables\Activity;
9+
10+
use OCP\Activity\IFilter;
11+
use OCP\IL10N;
12+
use OCP\IURLGenerator;
13+
14+
class Filter implements IFilter {
15+
public function __construct(
16+
protected IL10N $l,
17+
protected IURLGenerator $url,
18+
) {
19+
}
20+
21+
public function getIdentifier(): string {
22+
return ActivityConstants::APP_ID;
23+
}
24+
25+
public function getName(): string {
26+
return $this->l->t('Tables');
27+
}
28+
29+
public function getPriority(): int {
30+
return 40;
31+
}
32+
33+
public function getIcon(): string {
34+
return $this->url->getAbsoluteURL($this->url->imagePath(ActivityConstants::APP_ID, 'app-dark.svg'));
35+
}
36+
37+
public function filterTypes(array $types): array {
38+
return $types;
39+
}
40+
41+
public function allowedApps(): array {
42+
return [ActivityConstants::APP_ID];
43+
}
44+
}

lib/Activity/Provider.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
namespace OCA\Tables\Activity;
8+
9+
use OCP\Activity\Exceptions\UnknownActivityException;
10+
use OCP\Activity\IEvent;
11+
use OCP\Activity\IManager;
12+
use OCP\Activity\IProvider;
13+
use OCP\Comments\ICommentsManager;
14+
use OCP\IL10N;
15+
use OCP\IURLGenerator;
16+
use OCP\IUserManager;
17+
use OCP\L10N\IFactory;
18+
19+
class Provider implements IProvider {
20+
protected ?IL10N $l = null;
21+
22+
public function __construct(
23+
protected IFactory $languageFactory,
24+
protected IURLGenerator $url,
25+
protected ICommentsManager $commentsManager,
26+
protected IUserManager $userManager,
27+
protected IManager $activityManager,
28+
) {
29+
}
30+
31+
/**
32+
* @param string $language
33+
* @param IEvent $event
34+
* @param IEvent|null $previousEvent
35+
* @return IEvent
36+
* @throws UnknownActivityException
37+
*/
38+
public function parse($language, IEvent $event, ?IEvent $previousEvent = null): IEvent {
39+
if ($event->getApp() !== ActivityConstants::APP_ID) {
40+
throw new UnknownActivityException();
41+
}
42+
43+
$this->l = $this->languageFactory->get(ActivityConstants::APP_ID, $language);
44+
45+
if ($event->getSubject() === ActivityConstants::SUBJECT_IMPORT_FINISHED) {
46+
// $event->setParsedMessage($comment->getMessage())
47+
// ->setRichMessage($message, $mentions);
48+
49+
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath(ActivityConstants::APP_ID, 'app-dark.svg')));
50+
51+
if ($this->activityManager->isFormattingFilteredObject()) {
52+
try {
53+
return $this->parseShortVersion($event);
54+
} catch (UnknownActivityException) {
55+
// Ignore and simply use the long version...
56+
}
57+
}
58+
59+
return $this->parseLongVersion($event);
60+
}
61+
62+
throw new UnknownActivityException();
63+
}
64+
65+
/**
66+
* @throws UnknownActivityException
67+
*/
68+
protected function parseShortVersion(IEvent $event): IEvent {
69+
$subjectParameters = $this->getSubjectParameters($event);
70+
71+
if ($event->getSubject() === ActivityConstants::SUBJECT_IMPORT_FINISHED) {
72+
$event->setRichSubject($this->l->t('You commented'), []);
73+
} else {
74+
throw new UnknownActivityException();
75+
}
76+
77+
return $event;
78+
}
79+
80+
/**
81+
* @throws UnknownActivityException
82+
*/
83+
protected function parseLongVersion(IEvent $event): IEvent {
84+
$subjectParameters = $this->getSubjectParameters($event);
85+
86+
if ($event->getSubject() === ActivityConstants::SUBJECT_IMPORT_FINISHED) {
87+
$event->setParsedSubject($this->l->t('You commented on %1$s', [
88+
$subjectParameters['filePath'],
89+
]))
90+
->setRichSubject($this->l->t('You commented on {file}'), [
91+
'file' => $this->generateFileParameter($subjectParameters['fileId'], $subjectParameters['filePath']),
92+
]);
93+
} else {
94+
throw new UnknownActivityException();
95+
}
96+
97+
return $event;
98+
}
99+
100+
protected function getSubjectParameters(IEvent $event): array {
101+
$subjectParameters = $event->getSubjectParameters();
102+
if (isset($subjectParameters['fileId'])) {
103+
return $subjectParameters;
104+
}
105+
106+
return [
107+
'actor' => $subjectParameters[0],
108+
'fileId' => $event->getObjectId(),
109+
'filePath' => trim($subjectParameters[1], '/'),
110+
];
111+
}
112+
113+
/**
114+
* @return array<string, string>
115+
*/
116+
protected function generateFileParameter(int $id, string $path): array {
117+
return [
118+
'type' => 'file',
119+
'id' => (string)$id,
120+
'name' => basename($path),
121+
'path' => $path,
122+
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $id]),
123+
];
124+
}
125+
}

lib/Activity/Setting.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
namespace OCA\Tables\Activity;
8+
9+
use OCP\Activity\ActivitySettings;
10+
use OCP\IL10N;
11+
12+
class Setting extends ActivitySettings {
13+
public function __construct(
14+
protected IL10N $l,
15+
) {
16+
}
17+
18+
public function getIdentifier(): string {
19+
return ActivityConstants::TYPE_IMPORT_FINISHED;
20+
}
21+
22+
public function getName(): string {
23+
return $this->l->t('<strong>Import</strong> of a file has finished');
24+
}
25+
26+
public function getGroupIdentifier() {
27+
return ActivityConstants::APP_ID;
28+
}
29+
30+
public function getGroupName() {
31+
return $this->l->t('Tables');
32+
}
33+
34+
public function getPriority(): int {
35+
return 50;
36+
}
37+
38+
public function canChangeStream(): bool {
39+
return true;
40+
}
41+
42+
public function isDefaultEnabledStream(): bool {
43+
return true;
44+
}
45+
46+
public function canChangeMail(): bool {
47+
return true;
48+
}
49+
50+
public function isDefaultEnabledMail(): bool {
51+
return false;
52+
}
53+
}

lib/BackgroundJob/ImportTableJob.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Tables\BackgroundJob;
9+
10+
use OCA\Tables\Activity\ActivityManager;
11+
use OCA\Tables\Service\ImportService;
12+
use OCP\AppFramework\Utility\ITimeFactory;
13+
use OCP\BackgroundJob\QueuedJob;
14+
use OCP\IUserManager;
15+
use OCP\IUserSession;
16+
17+
class ImportTableJob extends QueuedJob {
18+
public function __construct(
19+
ITimeFactory $time,
20+
private IUserManager $userManager,
21+
private IUserSession $userSession,
22+
private ImportService $importService,
23+
private ActivityManager $activityManager,
24+
) {
25+
parent::__construct($time);
26+
}
27+
28+
/**
29+
* @param array $argument
30+
*/
31+
public function run($argument): void {
32+
$userId = $argument['user_id'];
33+
$tableId = $argument['table_id'];
34+
$viewId = $argument['view_id'];
35+
$path = $argument['path'];
36+
$createMissingColumns = $argument['create_missing_columns'] ?? false;
37+
$columnsConfig = $argument['columns_config'] ?? null;
38+
39+
$oldUser = $this->userSession->getUser();
40+
try {
41+
$user = $this->userManager->get($userId);
42+
$this->userSession->setUser($user);
43+
44+
//fixme: handle errors
45+
$importStats = $this->importService
46+
->import($userId, $tableId, $viewId, $path, $createMissingColumns, $columnsConfig);
47+
} catch (\Throwable $e) {
48+
throw $e;
49+
} finally {
50+
$this->userSession->setUser($oldUser);
51+
}
52+
53+
$this->activityManager->notifyImportFinished($userId, $tableId, $importStats);
54+
}
55+
}

lib/Controller/Api1Controller.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,7 +1373,7 @@ public function deleteRowByView(int $rowId, int $viewId): DataResponse {
13731373
public function importInTable(int $tableId, string $path, bool $createMissingColumns = true): DataResponse {
13741374
try {
13751375
// minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer
1376-
return new DataResponse($this->importService->import($tableId, null, $path, $createMissingColumns));
1376+
return new DataResponse($this->importService->scheduleImport($tableId, null, $path, $createMissingColumns));
13771377
} catch (PermissionError $e) {
13781378
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
13791379
$message = ['message' => $e->getMessage()];
@@ -1408,7 +1408,7 @@ public function importInTable(int $tableId, string $path, bool $createMissingCol
14081408
public function importInView(int $viewId, string $path, bool $createMissingColumns = true): DataResponse {
14091409
try {
14101410
// minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer
1411-
return new DataResponse($this->importService->import(null, $viewId, $path, $createMissingColumns));
1411+
return new DataResponse($this->importService->scheduleImport(null, $viewId, $path, $createMissingColumns));
14121412
} catch (PermissionError $e) {
14131413
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
14141414
$message = ['message' => $e->getMessage()];

0 commit comments

Comments
 (0)