0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-01-31 06:43:12 +00:00
nextcloud_server/lib/private/FilesMetadata/Model/FilesMetadata.php
Robin Appelman be7b6a7b1b
test: add some minimal testing for metadata storage
Signed-off-by: Robin Appelman <robin@icewind.nl>
2024-12-05 10:35:24 +01:00

638 lines
16 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
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 = '';
private ?int $storageId = null;
public function __construct(
private int $fileId = 0
) {
}
/**
* @inheritDoc
* @return int related file id
* @since 28.0.0
*/
public function getFileId(): int {
return $this->fileId;
}
public function getStorageId(): ?int {
return $this->storageId;
}
/**
* Set which storage the file this metadata belongs to.
*
* This helps with sharded filecache setups to know where to store the metadata
*
* @param int $storageId
* @return void
*/
public function setStorageId(int $storageId): void {
$this->storageId = $storageId;
}
/**
* @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);
}
public function getEtag(string $key): string {
if (!array_key_exists($key, $this->metadata)) {
return '';
}
return $this->metadata[$key]->getEtag();
}
public function setEtag(string $key, string $etag): void {
if (!array_key_exists($key, $this->metadata)) {
throw new FilesMetadataNotFoundException();
}
$this->metadata[$key]->setEtag($etag);
}
/**
* @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_INT_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();
}
}
}