migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,347 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata;
use JsonException;
use OC\FilesMetadata\Job\UpdateSingleMetadata;
use OC\FilesMetadata\Listener\MetadataDelete;
use OC\FilesMetadata\Listener\MetadataUpdate;
use OC\FilesMetadata\Model\FilesMetadata;
use OC\FilesMetadata\Service\IndexRequestService;
use OC\FilesMetadata\Service\MetadataRequestService;
use OCP\BackgroundJob\IJobList;
use OCP\DB\Exception;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\InvalidPathException;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
use OCP\FilesMetadata\Event\MetadataLiveEvent;
use OCP\FilesMetadata\Event\MetadataNamedEvent;
use OCP\FilesMetadata\Exceptions\FilesMetadataException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\FilesMetadata\IMetadataQuery;
use OCP\FilesMetadata\Model\IFilesMetadata;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IConfig;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* @inheritDoc
* @since 28.0.0
*/
class FilesMetadataManager implements IFilesMetadataManager {
public const CONFIG_KEY = 'files_metadata';
public const MIGRATION_DONE = 'files_metadata_installed';
private const JSON_MAXSIZE = 100000;
private ?IFilesMetadata $all = null;
public function __construct(
private IEventDispatcher $eventDispatcher,
private IJobList $jobList,
private IConfig $config,
private LoggerInterface $logger,
private MetadataRequestService $metadataRequestService,
private IndexRequestService $indexRequestService,
) {
}
/**
* @inheritDoc
*
* @param Node $node related node
* @param int $process type of process
*
* @return IFilesMetadata
* @throws FilesMetadataException if metadata are invalid
* @throws InvalidPathException if path to file is not valid
* @throws NotFoundException if file cannot be found
* @see self::PROCESS_BACKGROUND
* @see self::PROCESS_LIVE
* @since 28.0.0
*/
public function refreshMetadata(
Node $node,
int $process = self::PROCESS_LIVE,
string $namedEvent = ''
): IFilesMetadata {
try {
$metadata = $this->metadataRequestService->getMetadataFromFileId($node->getId());
} catch (FilesMetadataNotFoundException) {
$metadata = new FilesMetadata($node->getId());
}
// if $process is LIVE, we enforce LIVE
// if $process is NAMED, we go NAMED
// else BACKGROUND
if ((self::PROCESS_LIVE & $process) !== 0) {
$event = new MetadataLiveEvent($node, $metadata);
} elseif ((self::PROCESS_NAMED & $process) !== 0) {
$event = new MetadataNamedEvent($node, $metadata, $namedEvent);
} else {
$event = new MetadataBackgroundEvent($node, $metadata);
}
$this->eventDispatcher->dispatchTyped($event);
$this->saveMetadata($event->getMetadata());
// if requested, we add a new job for next cron to refresh metadata out of main thread
// if $process was set to LIVE+BACKGROUND, we run background process directly
if ($event instanceof MetadataLiveEvent && $event->isRunAsBackgroundJobRequested()) {
if ((self::PROCESS_BACKGROUND & $process) !== 0) {
return $this->refreshMetadata($node, self::PROCESS_BACKGROUND);
}
$this->jobList->add(UpdateSingleMetadata::class, [$node->getOwner()->getUID(), $node->getId()]);
}
return $metadata;
}
/**
* @param int $fileId file id
* @param boolean $generate Generate if metadata does not exists
*
* @inheritDoc
* @return IFilesMetadata
* @throws FilesMetadataNotFoundException if not found
* @since 28.0.0
*/
public function getMetadata(int $fileId, bool $generate = false): IFilesMetadata {
try {
return $this->metadataRequestService->getMetadataFromFileId($fileId);
} catch (FilesMetadataNotFoundException $ex) {
if ($generate) {
return new FilesMetadata($fileId);
}
throw $ex;
}
}
/**
* returns metadata of multiple file ids
*
* @param int[] $fileIds file ids
*
* @return array File ID is the array key, files without metadata are not returned in the array
* @psalm-return array<int, IFilesMetadata>
* @since 28.0.0
*/
public function getMetadataForFiles(array $fileIds): array {
return $this->metadataRequestService->getMetadataFromFileIds($fileIds);
}
/**
* @param IFilesMetadata $filesMetadata metadata
*
* @inheritDoc
* @throws FilesMetadataException if metadata seems malformed
* @since 28.0.0
*/
public function saveMetadata(IFilesMetadata $filesMetadata): void {
if ($filesMetadata->getFileId() === 0 || !$filesMetadata->updated()) {
return;
}
$json = json_encode($filesMetadata->jsonSerialize());
if (strlen($json) > self::JSON_MAXSIZE) {
throw new FilesMetadataException('json cannot exceed ' . self::JSON_MAXSIZE . ' characters long');
}
try {
if ($filesMetadata->getSyncToken() === '') {
$this->metadataRequestService->store($filesMetadata);
} else {
$this->metadataRequestService->updateMetadata($filesMetadata);
}
} catch (DBException $e) {
// most of the logged exception are the result of race condition
// between 2 simultaneous process trying to create/update metadata
$this->logger->warning('issue while saveMetadata', ['exception' => $e, 'metadata' => $filesMetadata]);
return;
}
// update indexes
foreach ($filesMetadata->getIndexes() as $index) {
try {
$this->indexRequestService->updateIndex($filesMetadata, $index);
} catch (DBException $e) {
$this->logger->warning('issue while updateIndex', ['exception' => $e]);
}
}
// update metadata types list
$current = $this->getKnownMetadata();
$current->import($filesMetadata->jsonSerialize(true));
$this->config->setAppValue('core', self::CONFIG_KEY, json_encode($current));
}
/**
* @param int $fileId file id
*
* @inheritDoc
* @since 28.0.0
*/
public function deleteMetadata(int $fileId): void {
try {
$this->metadataRequestService->dropMetadata($fileId);
} catch (Exception $e) {
$this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileId' => $fileId]);
}
try {
$this->indexRequestService->dropIndex($fileId);
} catch (Exception $e) {
$this->logger->warning('issue while deleteMetadata', ['exception' => $e, 'fileId' => $fileId]);
}
}
/**
* @param IQueryBuilder $qb
* @param string $fileTableAlias alias of the table that contains data about files
* @param string $fileIdField alias of the field that contains file ids
*
* @inheritDoc
* @return IMetadataQuery|null
* @see IMetadataQuery
* @since 28.0.0
*/
public function getMetadataQuery(
IQueryBuilder $qb,
string $fileTableAlias,
string $fileIdField
): ?IMetadataQuery {
if (!$this->metadataInitiated()) {
return null;
}
return new MetadataQuery($qb, $this->getKnownMetadata(), $fileTableAlias, $fileIdField);
}
/**
* @inheritDoc
* @return IFilesMetadata
* @since 28.0.0
*/
public function getKnownMetadata(): IFilesMetadata {
if (null !== $this->all) {
return $this->all;
}
$this->all = new FilesMetadata();
try {
$data = json_decode($this->config->getAppValue('core', self::CONFIG_KEY, '[]'), true, 127, JSON_THROW_ON_ERROR);
$this->all->import($data);
} catch (JsonException) {
$this->logger->warning('issue while reading stored list of metadata. Advised to run ./occ files:scan --all --generate-metadata');
}
return $this->all;
}
/**
* @param string $key metadata key
* @param string $type metadata type
* @param bool $indexed TRUE if metadata can be search
* @param int $editPermission remote edit permission via Webdav PROPPATCH
*
* @inheritDoc
* @since 28.0.0
* @see IMetadataValueWrapper::TYPE_INT
* @see IMetadataValueWrapper::TYPE_FLOAT
* @see IMetadataValueWrapper::TYPE_BOOL
* @see IMetadataValueWrapper::TYPE_ARRAY
* @see IMetadataValueWrapper::TYPE_STRING_LIST
* @see IMetadataValueWrapper::TYPE_INT_LIST
* @see IMetadataValueWrapper::TYPE_STRING
* @see IMetadataValueWrapper::EDIT_FORBIDDEN
* @see IMetadataValueWrapper::EDIT_REQ_OWNERSHIP
* @see IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION
* @see IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION
*/
public function initMetadata(
string $key,
string $type,
bool $indexed = false,
int $editPermission = IMetadataValueWrapper::EDIT_FORBIDDEN
): void {
$current = $this->getKnownMetadata();
try {
if ($current->getType($key) === $type
&& $indexed === $current->isIndex($key)
&& $editPermission === $current->getEditPermission($key)) {
return; // if key exists, with same type and indexed, we do nothing.
}
} catch (FilesMetadataNotFoundException) {
// if value does not exist, we keep on the writing of course
}
$current->import([$key => ['type' => $type, 'indexed' => $indexed, 'editPermission' => $editPermission]]);
$this->config->setAppValue('core', self::CONFIG_KEY, json_encode($current));
$this->all = $current;
}
/**
* load listeners
*
* @param IEventDispatcher $eventDispatcher
*/
public static function loadListeners(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addServiceListener(NodeWrittenEvent::class, MetadataUpdate::class);
$eventDispatcher->addServiceListener(CacheEntryRemovedEvent::class, MetadataDelete::class);
}
/**
* Will confirm that tables were created and store an app value to cache the result.
* Can be removed in 29 as this is to avoid strange situation when Nextcloud files were
* replaced but the upgrade was not triggered yet.
*
* @return bool
*/
private function metadataInitiated(): bool {
if ($this->config->getAppValue('core', self::MIGRATION_DONE, '0') === '1') {
return true;
}
$dbConnection = \OCP\Server::get(IDBConnection::class);
if ($dbConnection->tableExists(MetadataRequestService::TABLE_METADATA)) {
$this->config->setAppValue('core', self::MIGRATION_DONE, '1');
return true;
}
return false;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Job;
use OC\FilesMetadata\FilesMetadataManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\Files\IRootFolder;
use OCP\FilesMetadata\Event\MetadataLiveEvent;
use OCP\FilesMetadata\IFilesMetadataManager;
use Psr\Log\LoggerInterface;
/**
* Simple background job, created when requested by an app during the
* dispatch of MetadataLiveEvent.
* This background job will re-run the event to refresh metadata on a non-live thread.
*
* @see MetadataLiveEvent::requestBackgroundJob()
* @since 28.0.0
*/
class UpdateSingleMetadata extends QueuedJob {
public function __construct(
ITimeFactory $time,
private IRootFolder $rootFolder,
private FilesMetadataManager $filesMetadataManager,
private LoggerInterface $logger
) {
parent::__construct($time);
}
protected function run($argument) {
[$userId, $fileId] = $argument;
try {
$node = $this->rootFolder->getUserFolder($userId)->getById($fileId);
if (count($node) > 0) {
$file = array_shift($node);
$this->filesMetadataManager->refreshMetadata($file, IFilesMetadataManager::PROCESS_BACKGROUND);
}
} catch (\Exception $e) {
$this->logger->warning('issue while running UpdateSingleMetadata', ['exception' => $e, 'userId' => $userId, 'fileId' => $fileId]);
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Listener;
use Exception;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Cache\CacheEntryRemovedEvent;
use OCP\FilesMetadata\IFilesMetadataManager;
use Psr\Log\LoggerInterface;
/**
* Handle file deletion event and remove stored metadata related to the deleted file
*
* @template-implements IEventListener<CacheEntryRemovedEvent>
*/
class MetadataDelete implements IEventListener {
public function __construct(
private IFilesMetadataManager $filesMetadataManager,
private LoggerInterface $logger
) {
}
public function handle(Event $event): void {
if (!($event instanceof CacheEntryRemovedEvent)) {
return;
}
try {
$nodeId = $event->getFileId();
if ($nodeId > 0) {
$this->filesMetadataManager->deleteMetadata($nodeId);
}
} catch (Exception $e) {
$this->logger->warning('issue while running MetadataDelete', ['exception' => $e]);
}
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Listener;
use Exception;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeCreatedEvent;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\FilesMetadata\IFilesMetadataManager;
use Psr\Log\LoggerInterface;
/**
* Handle file creation/modification events and initiate a new event related to the created/edited file.
* The generated new event is broadcast in order to obtain file related metadata from other apps.
* metadata will be stored in database.
*
* @template-implements IEventListener<NodeCreatedEvent|NodeWrittenEvent>
*/
class MetadataUpdate implements IEventListener {
public function __construct(
private IFilesMetadataManager $filesMetadataManager,
private LoggerInterface $logger
) {
}
/**
* @param Event $event
*/
public function handle(Event $event): void {
if (!($event instanceof NodeWrittenEvent)) {
return;
}
try {
$this->filesMetadataManager->refreshMetadata($event->getNode());
} catch (Exception $e) {
$this->logger->warning('issue while running MetadataUpdate', ['exception' => $e]);
}
}
}
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata;
use OC\FilesMetadata\Model\FilesMetadata;
use OC\FilesMetadata\Service\IndexRequestService;
use OC\FilesMetadata\Service\MetadataRequestService;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException;
use OCP\FilesMetadata\IMetadataQuery;
use OCP\FilesMetadata\Model\IFilesMetadata;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
/**
* @inheritDoc
* @since 28.0.0
*/
class MetadataQuery implements IMetadataQuery {
private array $knownJoinedIndex = [];
public function __construct(
private IQueryBuilder $queryBuilder,
private IFilesMetadata $knownMetadata,
private string $fileTableAlias = 'fc',
private string $fileIdField = 'fileid',
private string $alias = 'meta',
private string $aliasIndexPrefix = 'meta_index'
) {
}
/**
* @inheritDoc
* @see self::extractMetadata()
* @since 28.0.0
*/
public function retrieveMetadata(): void {
$this->queryBuilder->selectAlias($this->alias . '.json', 'meta_json');
$this->queryBuilder->selectAlias($this->alias . '.sync_token', 'meta_sync_token');
$this->queryBuilder->leftJoin(
$this->fileTableAlias, MetadataRequestService::TABLE_METADATA, $this->alias,
$this->queryBuilder->expr()->eq($this->fileTableAlias . '.' . $this->fileIdField, $this->alias . '.file_id')
);
}
/**
* @param array $row result row
*
* @inheritDoc
* @return IFilesMetadata metadata
* @see self::retrieveMetadata()
* @since 28.0.0
*/
public function extractMetadata(array $row): IFilesMetadata {
$fileId = (array_key_exists($this->fileIdField, $row)) ? $row[$this->fileIdField] : 0;
$metadata = new FilesMetadata((int)$fileId);
try {
$metadata->importFromDatabase($row, $this->alias . '_');
} catch (FilesMetadataNotFoundException) {
// can be ignored as files' metadata are optional and might not exist in database
}
return $metadata;
}
/**
* @param string $metadataKey metadata key
* @param bool $enforce limit the request only to existing metadata
*
* @inheritDoc
* @since 28.0.0
*/
public function joinIndex(string $metadataKey, bool $enforce = false): string {
if (array_key_exists($metadataKey, $this->knownJoinedIndex)) {
return $this->knownJoinedIndex[$metadataKey];
}
$aliasIndex = $this->aliasIndexPrefix . '_' . count($this->knownJoinedIndex);
$this->knownJoinedIndex[$metadataKey] = $aliasIndex;
$expr = $this->queryBuilder->expr();
$andX = $expr->andX($expr->eq($aliasIndex . '.file_id', $this->fileTableAlias . '.' . $this->fileIdField));
$andX->add($expr->eq($this->getMetadataKeyField($metadataKey), $this->queryBuilder->createNamedParameter($metadataKey)));
if ($enforce) {
$this->queryBuilder->innerJoin(
$this->fileTableAlias,
IndexRequestService::TABLE_METADATA_INDEX,
$aliasIndex,
$andX
);
} else {
$this->queryBuilder->leftJoin(
$this->fileTableAlias,
IndexRequestService::TABLE_METADATA_INDEX,
$aliasIndex,
$andX
);
}
return $aliasIndex;
}
/**
* @throws FilesMetadataNotFoundException
*/
private function joinedTableAlias(string $metadataKey): string {
if (!array_key_exists($metadataKey, $this->knownJoinedIndex)) {
throw new FilesMetadataNotFoundException('table related to ' . $metadataKey . ' not initiated, you need to use leftJoin() first.');
}
return $this->knownJoinedIndex[$metadataKey];
}
/**
* @inheritDoc
*
* @param string $metadataKey metadata key
*
* @return string table field
* @throws FilesMetadataNotFoundException
* @since 28.0.0
*/
public function getMetadataKeyField(string $metadataKey): string {
return $this->joinedTableAlias($metadataKey) . '.meta_key';
}
/**
* @inheritDoc
*
* @param string $metadataKey metadata key
*
* @return string table field
* @throws FilesMetadataNotFoundException if metadataKey is not known
* @throws FilesMetadataTypeException is metadataKey is not set as indexed
* @since 28.0.0
*/
public function getMetadataValueField(string $metadataKey): string {
return match ($this->knownMetadata->getType($metadataKey)) {
IMetadataValueWrapper::TYPE_STRING => $this->joinedTableAlias($metadataKey) . '.meta_value_string',
IMetadataValueWrapper::TYPE_INT, IMetadataValueWrapper::TYPE_BOOL => $this->joinedTableAlias($metadataKey) . '.meta_value_int',
default => throw new FilesMetadataTypeException('metadata is not set as indexed'),
};
}
}
@@ -0,0 +1,621 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Model;
use JsonException;
use OCP\FilesMetadata\Exceptions\FilesMetadataKeyFormatException;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException;
use OCP\FilesMetadata\Model\IFilesMetadata;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
/**
* Model that represent metadata linked to a specific file.
*
* @inheritDoc
* @since 28.0.0
*/
class FilesMetadata implements IFilesMetadata {
/** @var array<string, MetadataValueWrapper> */
private array $metadata = [];
private bool $updated = false;
private int $lastUpdate = 0;
private string $syncToken = '';
public function __construct(
private int $fileId = 0
) {
}
/**
* @inheritDoc
* @return int related file id
* @since 28.0.0
*/
public function getFileId(): int {
return $this->fileId;
}
/**
* @inheritDoc
* @return int timestamp
* @since 28.0.0
*/
public function lastUpdateTimestamp(): int {
return $this->lastUpdate;
}
/**
* @inheritDoc
* @return string token
* @since 28.0.0
*/
public function getSyncToken(): string {
return $this->syncToken;
}
/**
* @inheritDoc
* @return string[] list of keys
* @since 28.0.0
*/
public function getKeys(): array {
return array_keys($this->metadata);
}
/**
* @param string $needle metadata key to search
*
* @inheritDoc
* @return bool TRUE if key exist
* @since 28.0.0
*/
public function hasKey(string $needle): bool {
return (in_array($needle, $this->getKeys()));
}
/**
* @inheritDoc
* @return string[] list of indexes
* @since 28.0.0
*/
public function getIndexes(): array {
$indexes = [];
foreach ($this->getKeys() as $key) {
if ($this->metadata[$key]->isIndexed()) {
$indexes[] = $key;
}
}
return $indexes;
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return bool TRUE if key exists and is set as indexed
* @since 28.0.0
*/
public function isIndex(string $key): bool {
return $this->metadata[$key]?->isIndexed() ?? false;
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return int edit permission
* @throws FilesMetadataNotFoundException
* @since 28.0.0
*/
public function getEditPermission(string $key): int {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getEditPermission();
}
/**
* @param string $key metadata key
* @param int $permission edit permission
*
* @inheritDoc
* @throws FilesMetadataNotFoundException
* @since 28.0.0
*/
public function setEditPermission(string $key, int $permission): void {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
$this->metadata[$key]->setEditPermission($permission);
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return string metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getString(string $key): string {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueString();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return int metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getInt(string $key): int {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueInt();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return float metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getFloat(string $key): float {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueFloat();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return bool metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getBool(string $key): bool {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueBool();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return array metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getArray(string $key): array {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueArray();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return string[] metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getStringList(string $key): array {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueStringList();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return int[] metadata value
* @throws FilesMetadataNotFoundException
* @throws FilesMetadataTypeException
* @since 28.0.0
*/
public function getIntList(string $key): array {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getValueIntList();
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return string value type
* @throws FilesMetadataNotFoundException
* @see IMetadataValueWrapper::TYPE_STRING
* @see IMetadataValueWrapper::TYPE_INT
* @see IMetadataValueWrapper::TYPE_FLOAT
* @see IMetadataValueWrapper::TYPE_BOOL
* @see IMetadataValueWrapper::TYPE_ARRAY
* @see IMetadataValueWrapper::TYPE_STRING_LIST
* @see IMetadataValueWrapper::TYPE_INT_LIST
* @since 28.0.0
*/
public function getType(string $key): string {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
return $this->metadata[$key]->getType();
}
/**
* @param string $key metadata key
* @param string $value metadata value
* @param bool $index set TRUE if value must be indexed
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setString(string $key, string $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getString($key) === $value && $index === $this->isIndex($key)) {
return $this; // we ignore if value and index have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING);
$this->updated = true;
$this->metadata[$key] = $meta->setValueString($value)->setIndexed($index);
return $this;
}
/**
* @param string $key metadata key
* @param int $value metadata value
* @param bool $index set TRUE if value must be indexed
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setInt(string $key, int $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getInt($key) === $value && $index === $this->isIndex($key)) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_INT);
$this->metadata[$key] = $meta->setValueInt($value)->setIndexed($index);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
* @param float $value metadata value
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setFloat(string $key, float $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getFloat($key) === $value && $index === $this->isIndex($key)) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_FLOAT);
$this->metadata[$key] = $meta->setValueFloat($value)->setIndexed($index);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
* @param bool $value metadata value
* @param bool $index set TRUE if value must be indexed
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setBool(string $key, bool $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getBool($key) === $value && $index === $this->isIndex($key)) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_BOOL);
$this->metadata[$key] = $meta->setValueBool($value)->setIndexed($index);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
* @param array $value metadata value
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setArray(string $key, array $value): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getArray($key) === $value) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_ARRAY);
$this->metadata[$key] = $meta->setValueArray($value);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
* @param string[] $value metadata value
* @param bool $index set TRUE if each values from the list must be indexed
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setStringList(string $key, array $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getStringList($key) === $value) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST);
$this->metadata[$key] = $meta->setValueStringList($value)->setIndexed($index);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
* @param int[] $value metadata value
* @param bool $index set TRUE if each values from the list must be indexed
*
* @inheritDoc
* @return self
* @throws FilesMetadataKeyFormatException
* @since 28.0.0
*/
public function setIntList(string $key, array $value, bool $index = false): IFilesMetadata {
$this->confirmKeyFormat($key);
try {
if ($this->getIntList($key) === $value) {
return $this; // we ignore if value have not changed
}
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
// if value does not exist, or type has changed, we keep on the writing
}
$valueWrapper = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST);
$this->metadata[$key] = $valueWrapper->setValueIntList($value)->setIndexed($index);
$this->updated = true;
return $this;
}
/**
* @param string $key metadata key
*
* @inheritDoc
* @return self
* @since 28.0.0
*/
public function unset(string $key): IFilesMetadata {
if (!array_key_exists($key, $this->metadata)) {
return $this;
}
unset($this->metadata[$key]);
$this->updated = true;
return $this;
}
/**
* @param string $keyPrefix metadata key prefix
*
* @inheritDoc
* @return self
* @since 28.0.0
*/
public function removeStartsWith(string $keyPrefix): IFilesMetadata {
if ($keyPrefix === '') {
return $this;
}
foreach ($this->getKeys() as $key) {
if (str_starts_with($key, $keyPrefix)) {
$this->unset($key);
}
}
return $this;
}
/**
* @param string $key
*
* @return void
* @throws FilesMetadataKeyFormatException
*/
private function confirmKeyFormat(string $key): void {
$acceptedChars = ['-', '_'];
if (ctype_alnum(str_replace($acceptedChars, '', $key))) {
return;
}
throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-, _)');
}
/**
* @inheritDoc
* @return bool TRUE if metadata have been modified
* @since 28.0.0
*/
public function updated(): bool {
return $this->updated;
}
public function jsonSerialize(bool $emptyValues = false): array {
$data = [];
foreach ($this->metadata as $metaKey => $metaValueWrapper) {
$data[$metaKey] = $metaValueWrapper->jsonSerialize($emptyValues);
}
return $data;
}
/**
* @return array<string, string|int|bool|float|string[]|int[]>
*/
public function asArray(): array {
$data = [];
foreach ($this->metadata as $metaKey => $metaValueWrapper) {
try {
$data[$metaKey] = $metaValueWrapper->getValueAny();
} catch (FilesMetadataNotFoundException $e) {
// ignore exception
}
}
return $data;
}
/**
* @param array $data
*
* @inheritDoc
* @return IFilesMetadata
* @since 28.0.0
*/
public function import(array $data): IFilesMetadata {
foreach ($data as $k => $v) {
$valueWrapper = new MetadataValueWrapper();
$this->metadata[$k] = $valueWrapper->import($v);
}
$this->updated = false;
return $this;
}
/**
* import data from database to configure this model
*
* @param array $data
* @param string $prefix
*
* @return IFilesMetadata
* @throws FilesMetadataNotFoundException
* @since 28.0.0
*/
public function importFromDatabase(array $data, string $prefix = ''): IFilesMetadata {
try {
$this->syncToken = $data[$prefix . 'sync_token'] ?? '';
return $this->import(
json_decode(
$data[$prefix . 'json'] ?? '[]',
true,
512,
JSON_THROW_ON_ERROR
)
);
} catch (JsonException) {
throw new FilesMetadataNotFoundException();
}
}
}
@@ -0,0 +1,421 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Model;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
/**
* @inheritDoc
* @see IFilesMetadata
* @since 28.0.0
*/
class MetadataValueWrapper implements IMetadataValueWrapper {
private string $type;
/** @var string|int|float|bool|array|string[]|int[] */
private mixed $value = null;
private bool $indexed = false;
private int $editPermission = self::EDIT_FORBIDDEN;
/**
* @param string $type value type
*
* @inheritDoc
* @see self::TYPE_INT
* @see self::TYPE_FLOAT
* @see self::TYPE_BOOL
* @see self::TYPE_ARRAY
* @see self::TYPE_STRING_LIST
* @see self::TYPE_INT_LIST
* @see self::TYPE_STRING
* @since 28.0.0
*/
public function __construct(string $type = '') {
$this->type = $type;
}
/**
* @inheritDoc
* @return string value type
* @see self::TYPE_INT
* @see self::TYPE_FLOAT
* @see self::TYPE_BOOL
* @see self::TYPE_ARRAY
* @see self::TYPE_STRING_LIST
* @see self::TYPE_INT_LIST
* @see self::TYPE_STRING
* @since 28.0.0
*/
public function getType(): string {
return $this->type;
}
/**
* @param string $type value type
*
* @inheritDoc
* @return bool
* @see self::TYPE_INT
* @see self::TYPE_FLOAT
* @see self::TYPE_BOOL
* @see self::TYPE_ARRAY
* @see self::TYPE_STRING_LIST
* @see self::TYPE_INT_LIST
* @see self::TYPE_STRING
* @since 28.0.0
*/
public function isType(string $type): bool {
return (strtolower($type) === strtolower($this->type));
}
/**
* @param string $type value type
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if type cannot be confirmed
* @see self::TYPE_INT
* @see self::TYPE_BOOL
* @see self::TYPE_ARRAY
* @see self::TYPE_STRING_LIST
* @see self::TYPE_INT_LIST
* @see self::TYPE_STRING
* @see self::TYPE_FLOAT
* @since 28.0.0
*/
public function assertType(string $type): self {
if (!$this->isType($type)) {
throw new FilesMetadataTypeException('type is \'' . $this->getType() . '\', expecting \'' . $type . '\'');
}
return $this;
}
/**
* @param string $value string to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store a string
* @since 28.0.0
*/
public function setValueString(string $value): self {
$this->assertType(self::TYPE_STRING);
$this->value = $value;
return $this;
}
/**
* @param int $value int to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store an int
* @since 28.0.0
*/
public function setValueInt(int $value): self {
$this->assertType(self::TYPE_INT);
$this->value = $value;
return $this;
}
/**
* @param float $value float to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store a float
* @since 28.0.0
*/
public function setValueFloat(float $value): self {
$this->assertType(self::TYPE_FLOAT);
$this->value = $value;
return $this;
}
/**
* @param bool $value bool to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store a bool
* @since 28.0.0
*/
public function setValueBool(bool $value): self {
$this->assertType(self::TYPE_BOOL);
$this->value = $value;
return $this;
}
/**
* @param array $value array to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store an array
* @since 28.0.0
*/
public function setValueArray(array $value): self {
$this->assertType(self::TYPE_ARRAY);
$this->value = $value;
return $this;
}
/**
* @param string[] $value string list to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store a string list
* @since 28.0.0
*/
public function setValueStringList(array $value): self {
$this->assertType(self::TYPE_STRING_LIST);
// TODO confirm value is an array or string ?
$this->value = $value;
return $this;
}
/**
* @param int[] $value int list to be set as value
*
* @inheritDoc
* @return self
* @throws FilesMetadataTypeException if wrapper was not set to store an int list
* @since 28.0.0
*/
public function setValueIntList(array $value): self {
$this->assertType(self::TYPE_INT_LIST);
// TODO confirm value is an array of int ?
$this->value = $value;
return $this;
}
/**
* @inheritDoc
* @return string set value
* @throws FilesMetadataTypeException if wrapper was not set to store a string
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueString(): string {
$this->assertType(self::TYPE_STRING);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (string)$this->value;
}
/**
* @inheritDoc
* @return int set value
* @throws FilesMetadataTypeException if wrapper was not set to store an int
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueInt(): int {
$this->assertType(self::TYPE_INT);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (int)$this->value;
}
/**
* @inheritDoc
* @return float set value
* @throws FilesMetadataTypeException if wrapper was not set to store a float
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueFloat(): float {
$this->assertType(self::TYPE_FLOAT);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (float)$this->value;
}
/**
* @inheritDoc
* @return bool set value
* @throws FilesMetadataTypeException if wrapper was not set to store a bool
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueBool(): bool {
$this->assertType(self::TYPE_BOOL);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (bool)$this->value;
}
/**
* @inheritDoc
* @return array set value
* @throws FilesMetadataTypeException if wrapper was not set to store an array
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueArray(): array {
$this->assertType(self::TYPE_ARRAY);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (array)$this->value;
}
/**
* @inheritDoc
* @return string[] set value
* @throws FilesMetadataTypeException if wrapper was not set to store a string list
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueStringList(): array {
$this->assertType(self::TYPE_STRING_LIST);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (array)$this->value;
}
/**
* @inheritDoc
* @return int[] set value
* @throws FilesMetadataTypeException if wrapper was not set to store an int list
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueIntList(): array {
$this->assertType(self::TYPE_INT_LIST);
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return (array)$this->value;
}
/**
* @inheritDoc
* @return string|int|float|bool|array|string[]|int[] set value
* @throws FilesMetadataNotFoundException if value is not set
* @since 28.0.0
*/
public function getValueAny(): mixed {
if (null === $this->value) {
throw new FilesMetadataNotFoundException('value is not set');
}
return $this->value;
}
/**
* @param bool $indexed TRUE to set the stored value as an indexed value
*
* @inheritDoc
* @return self
* @since 28.0.0
*/
public function setIndexed(bool $indexed): self {
$this->indexed = $indexed;
return $this;
}
/**
* @inheritDoc
* @return bool TRUE if value is an indexed value
* @since 28.0.0
*/
public function isIndexed(): bool {
return $this->indexed;
}
/**
* @param int $permission edit permission
*
* @inheritDoc
* @return self
* @since 28.0.0
*/
public function setEditPermission(int $permission): self {
$this->editPermission = $permission;
return $this;
}
/**
* @inheritDoc
* @return int edit permission
* @since 28.0.0
*/
public function getEditPermission(): int {
return $this->editPermission;
}
/**
* @param array $data serialized version of the object
*
* @inheritDoc
* @return self
* @see jsonSerialize
* @since 28.0.0
*/
public function import(array $data): self {
$this->value = $data['value'] ?? null;
$this->type = $data['type'] ?? '';
$this->setIndexed($data['indexed'] ?? false);
$this->setEditPermission($data['editPermission'] ?? self::EDIT_FORBIDDEN);
return $this;
}
public function jsonSerialize(bool $emptyValues = false): array {
return [
'value' => ($emptyValues) ? null : $this->value,
'type' => $this->getType(),
'indexed' => $this->isIndexed(),
'editPermission' => $this->getEditPermission()
];
}
}
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Service;
use OCP\DB\Exception as DbException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException;
use OCP\FilesMetadata\Model\IFilesMetadata;
use OCP\FilesMetadata\Model\IMetadataValueWrapper;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* manage sql request to the metadata_index table
*/
class IndexRequestService {
public const TABLE_METADATA_INDEX = 'files_metadata_index';
public function __construct(
private IDBConnection $dbConnection,
private LoggerInterface $logger
) {
}
/**
* update the index for a specific metadata key
*
* @param IFilesMetadata $filesMetadata metadata
* @param string $key metadata key to update
*
* @throws DbException
*/
public function updateIndex(IFilesMetadata $filesMetadata, string $key): void {
$fileId = $filesMetadata->getFileId();
try {
$metadataType = $filesMetadata->getType($key);
} catch (FilesMetadataNotFoundException $e) {
return;
}
/**
* might look harsh, but a lot simpler than comparing current indexed data, as we can expect
* conflict with a change of types.
* We assume that each time one random metadata were modified we can drop all index for this
* key and recreate them.
* To make it slightly cleaner, we'll use transaction
*/
$this->dbConnection->beginTransaction();
try {
$this->dropIndex($fileId, $key);
match ($metadataType) {
IMetadataValueWrapper::TYPE_STRING => $this->insertIndexString($fileId, $key, $filesMetadata->getString($key)),
IMetadataValueWrapper::TYPE_INT => $this->insertIndexInt($fileId, $key, $filesMetadata->getInt($key)),
IMetadataValueWrapper::TYPE_BOOL => $this->insertIndexBool($fileId, $key, $filesMetadata->getBool($key)),
IMetadataValueWrapper::TYPE_STRING_LIST => $this->insertIndexStringList($fileId, $key, $filesMetadata->getStringList($key)),
IMetadataValueWrapper::TYPE_INT_LIST => $this->insertIndexIntList($fileId, $key, $filesMetadata->getIntList($key))
};
} catch (FilesMetadataNotFoundException|FilesMetadataTypeException|DbException $e) {
$this->dbConnection->rollBack();
$this->logger->warning('issue while updateIndex', ['exception' => $e, 'fileId' => $fileId, 'key' => $key]);
}
$this->dbConnection->commit();
}
/**
* insert a new entry in the metadata_index table for a string value
*
* @param int $fileId file id
* @param string $key metadata key
* @param string $value metadata value
*
* @throws DbException
*/
private function insertIndexString(int $fileId, string $key, string $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
->setValue('meta_key', $qb->createNamedParameter($key))
->setValue('meta_value_string', $qb->createNamedParameter($value))
->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
/**
* insert a new entry in the metadata_index table for an int value
*
* @param int $fileId file id
* @param string $key metadata key
* @param int $value metadata value
*
* @throws DbException
*/
public function insertIndexInt(int $fileId, string $key, int $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
->setValue('meta_key', $qb->createNamedParameter($key))
->setValue('meta_value_int', $qb->createNamedParameter($value, IQueryBuilder::PARAM_INT))
->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
/**
* insert a new entry in the metadata_index table for a bool value
*
* @param int $fileId file id
* @param string $key metadata key
* @param bool $value metadata value
*
* @throws DbException
*/
public function insertIndexBool(int $fileId, string $key, bool $value): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA_INDEX)
->setValue('meta_key', $qb->createNamedParameter($key))
->setValue('meta_value_int', $qb->createNamedParameter(($value) ? '1' : '0', IQueryBuilder::PARAM_INT))
->setValue('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
$qb->executeStatement();
}
/**
* insert entries in the metadata_index table for list of string
*
* @param int $fileId file id
* @param string $key metadata key
* @param string[] $values metadata values
*
* @throws DbException
*/
public function insertIndexStringList(int $fileId, string $key, array $values): void {
foreach ($values as $value) {
$this->insertIndexString($fileId, $key, $value);
}
}
/**
* insert entries in the metadata_index table for list of int
*
* @param int $fileId file id
* @param string $key metadata key
* @param int[] $values metadata values
*
* @throws DbException
*/
public function insertIndexIntList(int $fileId, string $key, array $values): void {
foreach ($values as $value) {
$this->insertIndexInt($fileId, $key, $value);
}
}
/**
* drop indexes related to a file id
* if a key is specified, only drop entries related to it
*
* @param int $fileId file id
* @param string $key metadata key
*
* @throws DbException
*/
public function dropIndex(int $fileId, string $key = ''): void {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->delete(self::TABLE_METADATA_INDEX)
->where($expr->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
if ($key !== '') {
$qb->andWhere($expr->eq('meta_key', $qb->createNamedParameter($key)));
}
$qb->executeStatement();
}
}
@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @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\FilesMetadata\Service;
use OC\FilesMetadata\Model\FilesMetadata;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
use OCP\FilesMetadata\Model\IFilesMetadata;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* manage sql request to the metadata table
*/
class MetadataRequestService {
public const TABLE_METADATA = 'files_metadata';
public function __construct(
private IDBConnection $dbConnection,
private LoggerInterface $logger
) {
}
/**
* store metadata into database
*
* @param IFilesMetadata $filesMetadata
*
* @throws Exception
*/
public function store(IFilesMetadata $filesMetadata): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_METADATA)
->setValue('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT))
->setValue('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
->setValue('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
->setValue('last_update', (string) $qb->createFunction('NOW()'));
$qb->executeStatement();
}
/**
* returns metadata for a file id
*
* @param int $fileId file id
*
* @return IFilesMetadata
* @throws FilesMetadataNotFoundException if no metadata are found in database
*/
public function getMetadataFromFileId(int $fileId): IFilesMetadata {
try {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('json', 'sync_token')->from(self::TABLE_METADATA);
$qb->where(
$qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))
);
$result = $qb->executeQuery();
$data = $result->fetch();
$result->closeCursor();
} catch (Exception $e) {
$this->logger->warning(
'exception while getMetadataFromDatabase()', ['exception' => $e, 'fileId' => $fileId]
);
throw new FilesMetadataNotFoundException();
}
if ($data === false) {
throw new FilesMetadataNotFoundException();
}
$metadata = new FilesMetadata($fileId);
$metadata->importFromDatabase($data);
return $metadata;
}
/**
* returns metadata for multiple file ids
*
* If
*
* @param array $fileIds file ids
*
* @return array File ID is the array key, files without metadata are not returned in the array
* @psalm-return array<int, IFilesMetadata>
*/
public function getMetadataFromFileIds(array $fileIds): array {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('file_id', 'json', 'sync_token')->from(self::TABLE_METADATA);
$qb->where(
$qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY))
);
$list = [];
$result = $qb->executeQuery();
while ($data = $result->fetch()) {
$fileId = (int) $data['file_id'];
$metadata = new FilesMetadata($fileId);
try {
$metadata->importFromDatabase($data);
} catch (FilesMetadataNotFoundException) {
continue;
}
$list[$fileId] = $metadata;
}
$result->closeCursor();
return $list;
}
/**
* drop metadata related to a file id
*
* @param int $fileId file id
*
* @return void
* @throws Exception
*/
public function dropMetadata(int $fileId): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::TABLE_METADATA)
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();
}
/**
* update metadata in the database
*
* @param IFilesMetadata $filesMetadata metadata
*
* @return int number of affected rows
* @throws Exception
*/
public function updateMetadata(IFilesMetadata $filesMetadata): int {
$qb = $this->dbConnection->getQueryBuilder();
$expr = $qb->expr();
$qb->update(self::TABLE_METADATA)
->set('json', $qb->createNamedParameter(json_encode($filesMetadata->jsonSerialize())))
->set('sync_token', $qb->createNamedParameter($this->generateSyncToken()))
->set('last_update', $qb->createFunction('NOW()'))
->where(
$expr->andX(
$expr->eq('file_id', $qb->createNamedParameter($filesMetadata->getFileId(), IQueryBuilder::PARAM_INT)),
$expr->eq('sync_token', $qb->createNamedParameter($filesMetadata->getSyncToken()))
)
);
return $qb->executeStatement();
}
/**
* generate a random token
* @return string
*/
private function generateSyncToken(): string {
$chars = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890';
$str = '';
$max = strlen($chars);
for ($i = 0; $i < 7; $i++) {
try {
$str .= $chars[random_int(0, $max - 2)];
} catch (\Exception $e) {
$this->logger->warning('exception during generateSyncToken', ['exception' => $e]);
}
}
return $str;
}
}