* * @author Maxence Lange * * @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 . * */ 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 */ 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); } 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 */ 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(); } } }