Skip to content
Prev Previous commit
Next Next commit
Implement image caching
Signed-off-by: Julius Härtl <[email protected]>
  • Loading branch information
juliusknorr committed Aug 31, 2022
commit 0ce0d37ac18456092702a6ed4410ec7e61bdfc07
82 changes: 82 additions & 0 deletions core/Controller/ReferenceApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <[email protected]>
*
* @author Julius Härtl <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace OC\Core\Controller;

use OCP\AppFramework\Http\DataResponse;
use OC\Collaboration\Reference\ReferenceManager;
use OCP\IRequest;

class ReferenceApiController extends \OCP\AppFramework\OCSController {
private ReferenceManager $referenceManager;

public function __construct($appName, IRequest $request, ReferenceManager $referenceManager) {
parent::__construct($appName, $request);
$this->referenceManager = $referenceManager;
}

/**
* @NoAdminRequired
*
* @param string $text
* @param bool $resolve
* @return DataResponse
*/
public function extract(string $text, bool $resolve = false, int $limit = 1): DataResponse {
$references = $this->referenceManager->extractReferences($text);

$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ < $limit) {
$result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference) : null;
}
}

return new DataResponse([
'references' => $result
]);
}


/**
* @NoAdminRequired
*
* @param array $references
* @return DataResponse
*/
public function resolve(array $references, int $limit = 1): DataResponse {
$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ < $limit) {
$result[$reference] = $this->referenceManager->resolveReference($reference);
}
}

return new DataResponse([
'references' => array_filter($result)
]);
}
}
63 changes: 23 additions & 40 deletions core/Controller/ReferenceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,59 +24,42 @@

namespace OC\Core\Controller;

use OCP\AppFramework\Http\DataResponse;
use OC\Collaboration\Reference\ReferenceManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\NotFoundException;
use OCP\IRequest;

class ReferenceController extends \OCP\AppFramework\OCSController {
class ReferenceController extends \OCP\AppFramework\Controller {
private ReferenceManager $referenceManager;

public function __construct($appName, IRequest $request, ReferenceManager $referenceManager) {
public function __construct($appName, IRequest $request, ReferenceManager $referenceManager, IAppDataFactory $appDataFactory) {
parent::__construct($appName, $request);
$this->referenceManager = $referenceManager;
$this->appDataFactory = $appDataFactory;
}

/**
* @NoAdminRequired
*
* @param string $text
* @param bool $resolve
* @return DataResponse
* @PublicPage
* @NoCSRFRequired
* @param $referenceId
* @throws \OCP\Files\NotFoundException
*/
public function extract(string $text, bool $resolve = false, int $limit = 1): DataResponse {
$references = $this->referenceManager->extractReferences($text);

$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ < $limit) {
$result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference) : null;
}
public function preview($referenceId) {
$reference = $this->referenceManager->getReferenceByCacheKey($referenceId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get a hit here locally so I don't have any previews. Is there something else needed to configure? Is a Redis required? Or is the method not loading the providers or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, would require redis/apcu in its current state. I'd need to think how we can make this work without, as currently we have no reference from the hashed reference that is used for the preview to the original one.

if ($reference === null) {
return new DataResponse('', Http::STATUS_NOT_FOUND);
}

return new DataResponse([
'references' => $result
]);
}


/**
* @NoAdminRequired
*
* @param array $references
* @return DataResponse
*/
public function resolve(array $references, int $limit = 1): DataResponse {
$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ < $limit) {
$result[$reference] = $this->referenceManager->resolveReference($reference);
}
try {
$appData = $this->appDataFactory->get('core');
$folder = $appData->getFolder('opengraph');
$file = $folder->getFile($referenceId);
} catch (NotFoundException $e) {
return new DataResponse('', Http::STATUS_NOT_FOUND);
}

return new DataResponse([
'references' => array_filter($result)
]);
return new DataDownloadResponse($file->getContent(), $referenceId, $reference->getImageContentType());
}
}
5 changes: 3 additions & 2 deletions core/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
['name' => 'Preview#getPreviewByFileId', 'url' => '/core/preview', 'verb' => 'GET'],
['name' => 'Preview#getPreview', 'url' => '/core/preview.png', 'verb' => 'GET'],
['name' => 'RecommendedApps#index', 'url' => '/core/apps/recommended', 'verb' => 'GET'],
['name' => 'Reference#preview', 'url' => '/core/references/preview/{referenceId}', 'verb' => 'GET'],
['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'],
['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'],
['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'],
Expand Down Expand Up @@ -120,8 +121,8 @@
['root' => '/collaboration', 'name' => 'CollaborationResources#getCollectionsByResource', 'url' => '/resources/{resourceType}/{resourceId}', 'verb' => 'GET'],
['root' => '/collaboration', 'name' => 'CollaborationResources#createCollectionOnResource', 'url' => '/resources/{baseResourceType}/{baseResourceId}', 'verb' => 'POST'],

['root' => '/references', 'name' => 'Reference#extract', 'url' => '/extract', 'verb' => 'POST'],
['root' => '/references', 'name' => 'Reference#resolve', 'url' => '/resolve', 'verb' => 'POST'],
['root' => '/references', 'name' => 'ReferenceApi#extract', 'url' => '/extract', 'verb' => 'POST'],
['root' => '/references', 'name' => 'ReferenceApi#resolve', 'url' => '/resolve', 'verb' => 'POST'],

['root' => '/profile', 'name' => 'ProfileApi#setVisibility', 'url' => '/{targetUserId}', 'verb' => 'PUT'],

Expand Down
38 changes: 35 additions & 3 deletions lib/private/Collaboration/Reference/LinkReferenceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,36 @@
use OC\SystemConfig;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\IURLGenerator;
use Psr\Log\LoggerInterface;

class LinkReferenceProvider implements IReferenceProvider {
public const URL_PATTERN = '/(\s|^)(https?:\/\/)?((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|$)/i';

public const ALLOWED_CONTENT_TYPES = [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'image/webp'
];

private IClientService $clientService;
private LoggerInterface $logger;
private SystemConfig $systemConfig;
private IAppDataFactory $appDataFactory;
private IURLGenerator $urlGenerator;

public function __construct(IClientService $clientService, LoggerInterface $logger, SystemConfig $systemConfig) {
public function __construct(IClientService $clientService, LoggerInterface $logger, SystemConfig $systemConfig, IAppDataFactory $appDataFactory, IURLGenerator $urlGenerator) {
$this->clientService = $clientService;
$this->logger = $logger;
$this->systemConfig = $systemConfig;
$this->appDataFactory = $appDataFactory;
$this->urlGenerator = $urlGenerator;
}

public function matchReference(string $referenceText): bool {
Expand All @@ -65,7 +81,7 @@ public function resolveReference(string $referenceText): ?IReference {
private function fetchReference(Reference $reference) {
$client = $this->clientService->newClient();
try {
$response = $client->get($reference->getId());
$response = $client->get($reference->getId(), [ 'timeout' => 10 ]);
} catch (\Exception $e) {
$this->logger->debug('Failed to fetch link for obtaining open graph data', ['exception' => $e]);
return;
Expand All @@ -88,7 +104,23 @@ private function fetchReference(Reference $reference) {
}

if ($object->images) {
$reference->setImageUrl($object->images[0]->url);
try {
$appData = $this->appDataFactory->get('core');
try {
$folder = $appData->getFolder('opengraph');
} catch (NotFoundException $e) {
$folder = $appData->newFolder('opengraph');
}
$response = $client->get($object->images[0]->url, [ 'timeout' => 10 ]);
$contentType = $response->getHeader('Content-Type');
if (in_array($contentType, self::ALLOWED_CONTENT_TYPES, true)) {
$reference->setImageContentType($contentType);
$folder->newFile(md5($reference->getId()), $response->getBody());
$reference->setImageUrl($this->urlGenerator->linkToRouteAbsolute('core.Reference.preview', ['referenceId' => md5($reference->getId())]));
}
} catch (\Throwable $e) {
$this->logger->error('Failed to fetch and store the open graph image for ' . $reference->getId(), ['exception' => $e]);
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions lib/private/Collaboration/Reference/Reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Reference implements \OCP\Collaboration\Reference\IReference, \JsonSeriali
private ?string $title = null;
private ?string $description = null;
private ?string $imageUrl = null;
private ?string $contentType = null;
private ?string $url = null;

private ?string $richObjectType = null;
Expand Down Expand Up @@ -74,6 +75,14 @@ public function getImageUrl(): ?string {
return $this->imageUrl;
}

public function setImageContentType(?string $contentType): void {
$this->contentType = $contentType;
}

public function getImageContentType(): ?string {
return $this->contentType;
}

public function setUrl(?string $url): void {
$this->url = $url;
}
Expand Down Expand Up @@ -116,6 +125,7 @@ public static function toCache(Reference $reference): array {
'id' => $reference->getId(),
'title' => $reference->getTitle(),
'imageUrl' => $reference->getImageUrl(),
'imageContentType' => $reference->getImageContentType(),
'description' => $reference->getDescription(),
'link' => $reference->getUrl(),
'accessible' => $reference->accessible,
Expand All @@ -129,6 +139,7 @@ public static function fromCache(array $cache): Reference {
$reference->setTitle($cache['title']);
$reference->setDescription($cache['description']);
$reference->setImageUrl($cache['imageUrl']);
$reference->setImageContentType($cache['imageContentType']);
$reference->setUrl($cache['link']);
$reference->setRichObject($cache['richObjectType'], $cache['richObject']);
$reference->setAccessible($cache['accessible']);
Expand Down
9 changes: 9 additions & 0 deletions lib/private/Collaboration/Reference/ReferenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ public function extractReferences(string $text): array {
}, $references);
}

public function getReferenceByCacheKey(string $cacheKey): ?IReference {
$cached = $this->cache->get($cacheKey);
if ($cached) {
return Reference::fromCache($cached);
}

return null;
}

public function resolveReference(string $referenceId): ?IReference {
$matchedProvider = $this->getMatchedProvider($referenceId);

Expand Down