Dev changes

This commit is contained in:
Andrey Borysenko
2023-05-30 00:02:32 +03:00
parent a9d6ea8748
commit 3390d017cf
11 changed files with 233 additions and 28 deletions

View File

@ -47,6 +47,7 @@ return [
['name' => 'ocsApi#registerFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'POST'], ['name' => 'ocsApi#registerFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'POST'],
['name' => 'ocsApi#unregisterFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'DELETE'], ['name' => 'ocsApi#unregisterFileActionMenu', 'url' => '/api/v1/files/actions/menu', 'verb' => 'DELETE'],
['name' => 'ocsApi#handleFileAction', 'url' => '/api/v1/files/action', 'verb' => 'POST'], ['name' => 'ocsApi#handleFileAction', 'url' => '/api/v1/files/action', 'verb' => 'POST'],
['name' => 'ocsApi#loadFileActionIcon', 'url' => '/api/v1/files/action/icon', 'verb' => 'GET'],
// Notifications // Notifications
['name' => 'ocsApi#sendNotification', 'url' => '/api/v1/send-notification', 'verb' => 'POST'], ['name' => 'ocsApi#sendNotification', 'url' => '/api/v1/send-notification', 'verb' => 'POST'],

View File

@ -0,0 +1,4 @@
.icon-app-ecosystem-v2 {
background-image: url('../img/app-dark.svg');
filter: var(--background-invert-if-dark);
}

View File

@ -41,6 +41,7 @@ class Application extends App implements IBootstrap {
public const APP_ID = 'app_ecosystem_v2'; public const APP_ID = 'app_ecosystem_v2';
public const CACHE_TTL = 3600; public const CACHE_TTL = 3600;
public const ICON_CACHE_TTL = 60 * 60 *24;
public function __construct(array $urlParams = []) { public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams); parent::__construct(self::APP_ID, $urlParams);

View File

@ -31,7 +31,9 @@ declare(strict_types=1);
namespace OCA\AppEcosystemV2; namespace OCA\AppEcosystemV2;
use OCA\AppEcosystemV2\AppInfo\Application;
use OCA\AppEcosystemV2\Service\AppEcosystemV2Service; use OCA\AppEcosystemV2\Service\AppEcosystemV2Service;
use OCP\App\IAppManager;
use OCP\Capabilities\IPublicCapability; use OCP\Capabilities\IPublicCapability;
use OCP\IConfig; use OCP\IConfig;
@ -42,17 +44,23 @@ class Capabilities implements IPublicCapability {
/** @var AppEcosystemV2Service */ /** @var AppEcosystemV2Service */
private $service; private $service;
/** @var IAppManager */
private $appManager;
public function __construct( public function __construct(
IConfig $config, IConfig $config,
AppEcosystemV2Service $service, AppEcosystemV2Service $service,
IAppManager $appManager
) { ) {
$this->config = $config; $this->config = $config;
$this->service = $service; $this->service = $service;
$this->appManager = $appManager;
} }
public function getCapabilities(): array { public function getCapabilities(): array {
$capabilities = [ $capabilities = [
'nc-log-level' => $this->config->getSystemValue('loglevel', 2), 'nc-log-level' => $this->config->getSystemValue('loglevel', 2),
'app-ecosystem-version' => $this->appManager->getAppVersion(Application::APP_ID),
]; ];
return [ return [
'app_ecosystem_v2' => $capabilities, 'app_ecosystem_v2' => $capabilities,

View File

@ -42,6 +42,7 @@ use OCA\AppEcosystemV2\Db\ExApp;
use OCA\AppEcosystemV2\Service\AppEcosystemV2Service; use OCA\AppEcosystemV2\Service\AppEcosystemV2Service;
use OCA\AppEcosystemV2\Service\ExAppConfigService; use OCA\AppEcosystemV2\Service\ExAppConfigService;
use OCA\AppEcosystemV2\Service\ExFilesActionsMenuService; use OCA\AppEcosystemV2\Service\ExFilesActionsMenuService;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\IL10N; use OCP\IL10N;
@ -211,14 +212,44 @@ class OCSApiController extends OCSController {
], Http::STATUS_OK); ], Http::STATUS_OK);
} }
public function handleFileAction(string $appId, array $actionFile): JSONResponse { /**
// TODO * @NoAdminRequired
return new JSONResponse([ * @NoCSRFRequired
'success' => false, *
'appId' => $appId, * @param string $appId
'actionFile' => $actionFile, * @param string $actionName
'error' => 'Not implemented', * @param array $actionFile
], Http::STATUS_NOT_IMPLEMENTED); *
* @return JSONResponse
*/
public function handleFileAction(string $appId, string $actionName, array $actionFile, string $actionHandler): JSONResponse {
$result = $this->exFilesActionsMenuService->handleFileAction($appId, $actionName, $actionHandler, $actionFile);
return $this->buildResponse(new JSONResponse([
'success' => $result,
'handleFileActionSent' => $result,
], Http::STATUS_OK));
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @param string $url
*
* @return DataDisplayResponse
*/
public function loadFileActionIcon(string $url): DataDisplayResponse {
$icon = $this->exFilesActionsMenuService->loadFileActionIcon($url);
if ($icon !== null && isset($icon['body'], $icon['headers'])) {
$response = new DataDisplayResponse(
$icon['body'],
Http::STATUS_OK,
['Content-Type' => $icon['headers']['Content-Type'][0] ?? 'image/svg+xml']
);
$response->cacheFor(Application::ICON_CACHE_TTL, false, true);
return $response;
}
return new DataDisplayResponse('', 400);
} }
/** /**
@ -228,15 +259,13 @@ class OCSApiController extends OCSController {
* @param string $appId * @param string $appId
* @param string $configKey * @param string $configKey
* @param string $configValue * @param string $configValue
*
* @return JSONResponse
*/ */
public function setAppConfigValue(string $appId, string $configKey, string $configValue): JSONResponse { public function setAppConfigValue(string $appId, string $configKey, string $configValue, string $format = 'json') {
$result = $this->exAppConfigService->setAppConfigValue($appId, $configKey, $configValue); $result = $this->exAppConfigService->setAppConfigValue($appId, $configKey, $configValue);
return new JSONResponse([ return $this->buildResponse(new DataResponse([
'success' => $result !== null, 'success' => $result !== null,
'setAppConfigValue' => $result, 'setAppConfigValue' => $result,
], Http::STATUS_OK); ], Http::STATUS_OK), $format);
} }
/** /**
@ -245,15 +274,16 @@ class OCSApiController extends OCSController {
* *
* @param string $appId * @param string $appId
* @param string $configKey * @param string $configKey
* @param string $format
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function getAppConfigValue(string $appId, string $configKey): JSONResponse { public function getAppConfigValue(string $appId, string $configKey, string $format): JSONResponse {
$appConfigEx = $this->exAppConfigService->getAppConfigValue($appId, $configKey); $appConfigEx = $this->exAppConfigService->getAppConfigValue($appId, $configKey);
return new JSONResponse([ return $this->buildResponse(new JSONResponse([
'success' => $appConfigEx !== null, 'success' => $appConfigEx !== null,
'appConfigEx' => $appConfigEx, 'appConfigEx' => $appConfigEx,
], Http::STATUS_OK); ], Http::STATUS_OK), $format);
} }
/** /**

View File

@ -86,7 +86,34 @@ class ExFilesActionsMenuMapper extends QBMapper {
return $this->findEntity($qb); return $this->findEntity($qb);
} }
public function deleteByAppidName(ExFilesActionsMenu $exFilesActionsMenu) { /**
* @param ExFilesActionsMenu $exFilesActionsMenu
*
* @return int Number of updated rows
*/
public function updateFileActionMenu(ExFilesActionsMenu $exFilesActionsMenu) {
$qb = $this->db->getQueryBuilder();
return $qb->update($this->tableName)
->set('display_name', $qb->createNamedParameter($exFilesActionsMenu->getDisplayName(), IQueryBuilder::PARAM_STR))
->set('mime', $qb->createNamedParameter($exFilesActionsMenu->getMime(), IQueryBuilder::PARAM_STR))
->set('permissions', $qb->createNamedParameter($exFilesActionsMenu->getPermissions(), IQueryBuilder::PARAM_STR))
->set('order', $qb->createNamedParameter($exFilesActionsMenu->getOrder(), IQueryBuilder::PARAM_INT))
->set('icon', $qb->createNamedParameter($exFilesActionsMenu->getIcon() ?? '', IQueryBuilder::PARAM_STR))
->set('icon_class', $qb->createNamedParameter($exFilesActionsMenu->getIconClass(), IQueryBuilder::PARAM_STR))
->set('action_handler', $qb->createNamedParameter($exFilesActionsMenu->getActionHandler(), IQueryBuilder::PARAM_STR))
->where(
$qb->expr()->eq('appId', $qb->createNamedParameter($exFilesActionsMenu->getAppid(), IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('name', $qb->createNamedParameter($exFilesActionsMenu->getName(), IQueryBuilder::PARAM_STR))
)
->executeStatement();
}
/**
* @param ExFilesActionsMenu $exFilesActionsMenu
*
* @return int Number of deleted rows
*/
public function deleteByAppidName(ExFilesActionsMenu $exFilesActionsMenu): int {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
return $qb->delete($this->tableName) return $qb->delete($this->tableName)
->where( ->where(

View File

@ -56,7 +56,7 @@ class LoadFilesPluginListener implements IEventListener {
if (!empty($exFilesActions)) { if (!empty($exFilesActions)) {
$this->initialState->provideInitialState('ex_files_actions_menu', ['fileActions' => $exFilesActions]); $this->initialState->provideInitialState('ex_files_actions_menu', ['fileActions' => $exFilesActions]);
Util::addScript(Application::APP_ID, Application::APP_ID . '-filesplugin'); Util::addScript(Application::APP_ID, Application::APP_ID . '-filesplugin');
// Util::addStyle(Application::APP_ID, 'filesplugin'); Util::addStyle(Application::APP_ID, 'filesactions');
} }
} }
} }

View File

@ -130,18 +130,19 @@ class Version1000Date202305221555 extends SimpleMigrationStep {
$table->addColumn('appid', 'string', [ $table->addColumn('appid', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 32 'length' => 32,
]); ]);
$table->addColumn('name', 'string', [ $table->addColumn('name', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 64 'length' => 64,
]); ]);
$table->addColumn('display_name', 'string', [ $table->addColumn('display_name', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 64 'length' => 64,
]); ]);
$table->addColumn('mime', 'string', [ $table->addColumn('mime', 'string', [
'notnull' => true, 'notnull' => true,
'default' => 'file',
]); ]);
// https://nextcloud.github.io/nextcloud-files/enums/Permission.html // https://nextcloud.github.io/nextcloud-files/enums/Permission.html
$table->addColumn('permissions', 'string', [ $table->addColumn('permissions', 'string', [
@ -149,6 +150,7 @@ class Version1000Date202305221555 extends SimpleMigrationStep {
]); ]);
$table->addColumn('order', 'integer', [ $table->addColumn('order', 'integer', [
'notnull' => true, 'notnull' => true,
'default' => 0,
]); ]);
$table->addColumn('icon', 'string', [ $table->addColumn('icon', 'string', [
'notnull' => true, 'notnull' => true,
@ -156,15 +158,15 @@ class Version1000Date202305221555 extends SimpleMigrationStep {
]); ]);
$table->addColumn('icon_class', 'string', [ $table->addColumn('icon_class', 'string', [
'notnull' => true, 'notnull' => true,
'default' => '', 'default' => 'icon-app-ecosystem-v2',
]); ]);
// Action handler key name, that will sent to exApp for handling // Action handler key name, that will sent to exApp for handling
$table->addColumn('action_handler', 'string', [ $table->addColumn('action_handler', 'string', [
'notnull' => true, 'notnull' => true,
'length' => 64 'length' => 64,
]); ]);
$table->setPrimaryKey(['appid'], 'ex_files_actions_menu_appid'); $table->setPrimaryKey(['appid', 'name'], 'ex_files_actions_menu_pk');
$table->addIndex(['name'], 'ex_files_actions_menu_name'); $table->addIndex(['name'], 'ex_files_actions_menu_name');
} }

View File

@ -31,6 +31,7 @@ declare(strict_types=1);
namespace OCA\AppEcosystemV2\Service; namespace OCA\AppEcosystemV2\Service;
use OCA\AppEcosystemV2\AppInfo\Application;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use OCP\IConfig; use OCP\IConfig;
@ -40,7 +41,9 @@ use OCP\Http\Client\IClient;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\Entity;
use OCA\AppEcosystemV2\Db\ExApp; use OCA\AppEcosystemV2\Db\ExApp;
use OCA\AppEcosystemV2\Db\ExAppMapper; use OCA\AppEcosystemV2\Db\ExAppMapper;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IL10N;
class AppEcosystemV2Service { class AppEcosystemV2Service {
/** @var IConfig */ /** @var IConfig */
@ -55,16 +58,26 @@ class AppEcosystemV2Service {
/** @var ExAppMapper */ /** @var ExAppMapper */
private $exAppMapper; private $exAppMapper;
/** @var IL10N */
private $l10n;
/** @var IAppManager */
private $appManager;
public function __construct( public function __construct(
IConfig $config, IConfig $config,
LoggerInterface $logger, LoggerInterface $logger,
IClientService $clientService, IClientService $clientService,
ExAppMapper $exAppMapper, ExAppMapper $exAppMapper,
IL10N $l10n,
IAppManager $appManager,
) { ) {
$this->config = $config; $this->config = $config;
$this->logger = $logger; $this->logger = $logger;
$this->client = $clientService->newClient(); $this->client = $clientService->newClient();
$this->exAppMapper = $exAppMapper; $this->exAppMapper = $exAppMapper;
$this->l10n = $l10n;
$this->appManager = $appManager;
} }
public function getExApp(string $exAppId): ?Entity { public function getExApp(string $exAppId): ?Entity {
@ -182,4 +195,53 @@ class AppEcosystemV2Service {
// TODO // TODO
return []; return [];
} }
public function requestToExApp(ExApp $exApp, string $route, string $method = 'POST', array $params = []) {
try {
$exAppConfig = $exApp->getConfig();
$url = $exAppConfig['protocol'] . '://' . $exAppConfig['host'] . ':' . $exAppConfig['port'] . $route;
$options = [
'headers' => [
// TODO: Add authorization headers
'NC-VERSION' => $this->config->getSystemValue('version'),
'APP-ECOSYSTEM-VERSION' => $this->appManager->getAppVersion(Application::APP_ID, false),
],
];
if (count($params) > 0) {
if ($method === 'GET') {
// manage array parameters
$paramsContent = '';
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $oneArrayValue) {
$paramsContent .= $key . '[]=' . urlencode($oneArrayValue) . '&';
}
unset($params[$key]);
}
}
$paramsContent .= http_build_query($params);
$url .= '?' . $paramsContent;
} else {
$options['body'] = $params;
}
}
if ($method === 'GET') {
$response = $this->client->get($url, $options);
} else if ($method === 'POST') {
$response = $this->client->post($url, $options);
} else if ($method === 'PUT') {
$response = $this->client->put($url, $options);
} else if ($method === 'DELETE') {
$response = $this->client->delete($url, $options);
} else {
return ['error' => $this->l10n->t('Bad HTTP method')];
}
return $response;
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
}
} }

View File

@ -39,6 +39,10 @@ use OCA\AppEcosystemV2\Db\ExFilesActionsMenu;
use OCA\AppEcosystemV2\Db\ExFilesActionsMenuMapper; use OCA\AppEcosystemV2\Db\ExFilesActionsMenuMapper;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Http;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
class ExFilesActionsMenuService { class ExFilesActionsMenuService {
/** @var CappedMemoryCache */ /** @var CappedMemoryCache */
@ -50,14 +54,24 @@ class ExFilesActionsMenuService {
/** @var LoggerInterface */ /** @var LoggerInterface */
private $logger; private $logger;
/** @var IClient */
private $client;
/** @var AppEcosystemV2Service */
private $appEcosystemV2Service;
public function __construct( public function __construct(
CappedMemoryCache $cache, CappedMemoryCache $cache,
ExFilesActionsMenuMapper $mapper, ExFilesActionsMenuMapper $mapper,
LoggerInterface $logger, LoggerInterface $logger,
IClientService $clientService,
AppEcosystemV2Service $appEcosystemV2Service,
) { ) {
$this->cache = $cache; $this->cache = $cache;
$this->mapper = $mapper; $this->mapper = $mapper;
$this->logger = $logger; $this->logger = $logger;
$this->client = $clientService->newClient();
$this->appEcosystemV2Service = $appEcosystemV2Service;
} }
/** /**
@ -85,7 +99,10 @@ class ExFilesActionsMenuService {
$fileActionMenu->setIcon($params['icon']); $fileActionMenu->setIcon($params['icon']);
$fileActionMenu->setIconClass($params['icon_class']); $fileActionMenu->setIconClass($params['icon_class']);
$fileActionMenu->setActionHandler($params['action_handler']); $fileActionMenu->setActionHandler($params['action_handler']);
$this->mapper->update($fileActionMenu); if ($this->mapper->updateFileActionMenu($fileActionMenu) !== 1) {
$this->logger->error('Failed to update file action menu ' . $params['name'] . ' for app: ' . $appId);
return null;
}
} else { } else {
$fileActionMenu = $this->mapper->insert(new ExFilesActionsMenu([ $fileActionMenu = $this->mapper->insert(new ExFilesActionsMenu([
'appid' => $appId, 'appid' => $appId,
@ -145,4 +162,48 @@ class ExFilesActionsMenuService {
$this->cache->set($cacheKey, $fileActions, Application::CACHE_TTL); $this->cache->set($cacheKey, $fileActions, Application::CACHE_TTL);
return $fileActions; return $fileActions;
} }
public function handleFileAction(string $appId, string $fileActionName, string $actionHandler, array $actionFile): bool {
try {
$exFileAction = $this->mapper->findByName($fileActionName);
} catch (DoesNotExistException) {
$exFileAction = null;
}
if ($exFileAction !== null) {
$handler = $exFileAction->getActionHandler(); // route on ex app
$params = [
'actionName' => $fileActionName,
'actionFile' => [
'fileId' => $actionFile['fileId'],
'name' => $actionFile['name'],
'dir' => $actionFile['dir'],
],
'actionHandler' => $actionHandler,
];
$exApp = $this->appEcosystemV2Service->getExApp($appId);
if ($exApp !== null) {
$result = $this->appEcosystemV2Service->requestToExApp($exApp, $handler, 'POST', $params);
if ($result instanceof IResponse) {
return $result->getStatusCode() === 200;
}
if (isset($result['error'])) {
$this->logger->error('Failed to handle file action ' . $fileActionName . ' for app: ' . $appId . ' with error: ' . $result['error']);
return false;
}
}
}
$this->logger->error('Failed to find file action menu ' . $fileActionName . ' for app: ' . $appId);
return false;
}
public function loadFileActionIcon(string $url): array {
$thumbnailResponse = $this->client->get($url);
if ($thumbnailResponse->getStatusCode() === Http::STATUS_OK) {
return [
'body' => $thumbnailResponse->getBody(),
'headers' => $thumbnailResponse->getHeaders(),
];
}
return null;
}
} }

View File

@ -14,23 +14,32 @@ state.fileActions.forEach(fileAction => {
mime: fileAction.mime, mime: fileAction.mime,
permissions: Number(fileAction.permissions), permissions: Number(fileAction.permissions),
order: Number(fileAction.order), order: Number(fileAction.order),
icon: fileAction.icon, icon: fileAction.icon !== '' ? generateOcsUrl('/apps/app_ecosystem_v2/api/v1/files/action/icon?url=' + fileAction.icon) : null,
iconClass: fileAction.icon_class, iconClass: fileAction.icon_class,
actionHandler: (fileName, context) => { actionHandler: (fileName, context) => {
console.debug('file', fileName) console.debug('[AppEcosystemV2] file', fileName)
console.debug('context', context) console.debug('[AppEcosystemV2] context', context)
console.debug('[AppEcosystemV2] fileAction', fileAction)
axios.post(generateOcsUrl('/apps/app_ecosystem_v2/api/v1/files/action'), { axios.post(generateOcsUrl('/apps/app_ecosystem_v2/api/v1/files/action'), {
appId: fileAction.appid, appId: fileAction.appid,
actionName: fileAction.name,
actionFile: { actionFile: {
fileId: Number(context.$file[0].dataset.id), fileId: Number(context.$file[0].dataset.id),
name: fileName, name: fileName,
dir: context.$file[0].dataset.path, dir: context.$file[0].dataset.path,
}, },
actionHandler: fileAction.action_handler,
}).then((response) => { }).then((response) => {
console.debug('response', response) console.debug('response', response)
if (response.data.success) {
OC.dialogs.info(t(fileAction.appid, 'Action request sent to ExApp'), t(fileAction.appid, fileAction.display_name))
} else {
OC.dialogs.info(t(fileAction.appid, 'Error while sending action request to ExApp'), t(fileAction.appid, fileAction.display_name))
}
// TODO: Handle defined format of response for next actions (e.g. show notification, open dialog, etc.) // TODO: Handle defined format of response for next actions (e.g. show notification, open dialog, etc.)
}).catch((error) => { }).catch((error) => {
console.error('error', error) console.error('error', error)
OC.dialogs.info(t(fileAction.appid, 'Error while sending action request to ExApp'), t(fileAction.appid, fileAction.display_name))
}) })
}, },
} }