FilesMetadata.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright 2023 Maxence Lange <maxence@artificial-owl.com>
  5. *
  6. * @author Maxence Lange <maxence@artificial-owl.com>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OC\FilesMetadata\Model;
  25. use JsonException;
  26. use OCP\FilesMetadata\Exceptions\FilesMetadataKeyFormatException;
  27. use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
  28. use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException;
  29. use OCP\FilesMetadata\Model\IFilesMetadata;
  30. use OCP\FilesMetadata\Model\IMetadataValueWrapper;
  31. /**
  32. * Model that represent metadata linked to a specific file.
  33. *
  34. * @inheritDoc
  35. * @since 28.0.0
  36. */
  37. class FilesMetadata implements IFilesMetadata {
  38. /** @var array<string, MetadataValueWrapper> */
  39. private array $metadata = [];
  40. private bool $updated = false;
  41. private int $lastUpdate = 0;
  42. private string $syncToken = '';
  43. public function __construct(
  44. private int $fileId = 0
  45. ) {
  46. }
  47. /**
  48. * @inheritDoc
  49. * @return int related file id
  50. * @since 28.0.0
  51. */
  52. public function getFileId(): int {
  53. return $this->fileId;
  54. }
  55. /**
  56. * @inheritDoc
  57. * @return int timestamp
  58. * @since 28.0.0
  59. */
  60. public function lastUpdateTimestamp(): int {
  61. return $this->lastUpdate;
  62. }
  63. /**
  64. * @inheritDoc
  65. * @return string token
  66. * @since 28.0.0
  67. */
  68. public function getSyncToken(): string {
  69. return $this->syncToken;
  70. }
  71. /**
  72. * @inheritDoc
  73. * @return string[] list of keys
  74. * @since 28.0.0
  75. */
  76. public function getKeys(): array {
  77. return array_keys($this->metadata);
  78. }
  79. /**
  80. * @param string $needle metadata key to search
  81. *
  82. * @inheritDoc
  83. * @return bool TRUE if key exist
  84. * @since 28.0.0
  85. */
  86. public function hasKey(string $needle): bool {
  87. return (in_array($needle, $this->getKeys()));
  88. }
  89. /**
  90. * @inheritDoc
  91. * @return string[] list of indexes
  92. * @since 28.0.0
  93. */
  94. public function getIndexes(): array {
  95. $indexes = [];
  96. foreach ($this->getKeys() as $key) {
  97. if ($this->metadata[$key]->isIndexed()) {
  98. $indexes[] = $key;
  99. }
  100. }
  101. return $indexes;
  102. }
  103. /**
  104. * @param string $key metadata key
  105. *
  106. * @inheritDoc
  107. * @return bool TRUE if key exists and is set as indexed
  108. * @since 28.0.0
  109. */
  110. public function isIndex(string $key): bool {
  111. return $this->metadata[$key]?->isIndexed() ?? false;
  112. }
  113. /**
  114. * @param string $key metadata key
  115. *
  116. * @inheritDoc
  117. * @return int edit permission
  118. * @throws FilesMetadataNotFoundException
  119. * @since 28.0.0
  120. */
  121. public function getEditPermission(string $key): int {
  122. if (!array_key_exists($key, $this->metadata)) {
  123. throw new FilesMetadataNotFoundException();
  124. }
  125. return $this->metadata[$key]->getEditPermission();
  126. }
  127. /**
  128. * @param string $key metadata key
  129. * @param int $permission edit permission
  130. *
  131. * @inheritDoc
  132. * @throws FilesMetadataNotFoundException
  133. * @since 28.0.0
  134. */
  135. public function setEditPermission(string $key, int $permission): void {
  136. if (!array_key_exists($key, $this->metadata)) {
  137. throw new FilesMetadataNotFoundException();
  138. }
  139. $this->metadata[$key]->setEditPermission($permission);
  140. }
  141. public function getEtag(string $key): string {
  142. if (!array_key_exists($key, $this->metadata)) {
  143. return '';
  144. }
  145. return $this->metadata[$key]->getEtag();
  146. }
  147. public function setEtag(string $key, string $etag): void {
  148. if (!array_key_exists($key, $this->metadata)) {
  149. throw new FilesMetadataNotFoundException();
  150. }
  151. $this->metadata[$key]->setEtag($etag);
  152. }
  153. /**
  154. * @param string $key metadata key
  155. *
  156. * @inheritDoc
  157. * @return string metadata value
  158. * @throws FilesMetadataNotFoundException
  159. * @throws FilesMetadataTypeException
  160. * @since 28.0.0
  161. */
  162. public function getString(string $key): string {
  163. if (!array_key_exists($key, $this->metadata)) {
  164. throw new FilesMetadataNotFoundException();
  165. }
  166. return $this->metadata[$key]->getValueString();
  167. }
  168. /**
  169. * @param string $key metadata key
  170. *
  171. * @inheritDoc
  172. * @return int metadata value
  173. * @throws FilesMetadataNotFoundException
  174. * @throws FilesMetadataTypeException
  175. * @since 28.0.0
  176. */
  177. public function getInt(string $key): int {
  178. if (!array_key_exists($key, $this->metadata)) {
  179. throw new FilesMetadataNotFoundException();
  180. }
  181. return $this->metadata[$key]->getValueInt();
  182. }
  183. /**
  184. * @param string $key metadata key
  185. *
  186. * @inheritDoc
  187. * @return float metadata value
  188. * @throws FilesMetadataNotFoundException
  189. * @throws FilesMetadataTypeException
  190. * @since 28.0.0
  191. */
  192. public function getFloat(string $key): float {
  193. if (!array_key_exists($key, $this->metadata)) {
  194. throw new FilesMetadataNotFoundException();
  195. }
  196. return $this->metadata[$key]->getValueFloat();
  197. }
  198. /**
  199. * @param string $key metadata key
  200. *
  201. * @inheritDoc
  202. * @return bool metadata value
  203. * @throws FilesMetadataNotFoundException
  204. * @throws FilesMetadataTypeException
  205. * @since 28.0.0
  206. */
  207. public function getBool(string $key): bool {
  208. if (!array_key_exists($key, $this->metadata)) {
  209. throw new FilesMetadataNotFoundException();
  210. }
  211. return $this->metadata[$key]->getValueBool();
  212. }
  213. /**
  214. * @param string $key metadata key
  215. *
  216. * @inheritDoc
  217. * @return array metadata value
  218. * @throws FilesMetadataNotFoundException
  219. * @throws FilesMetadataTypeException
  220. * @since 28.0.0
  221. */
  222. public function getArray(string $key): array {
  223. if (!array_key_exists($key, $this->metadata)) {
  224. throw new FilesMetadataNotFoundException();
  225. }
  226. return $this->metadata[$key]->getValueArray();
  227. }
  228. /**
  229. * @param string $key metadata key
  230. *
  231. * @inheritDoc
  232. * @return string[] metadata value
  233. * @throws FilesMetadataNotFoundException
  234. * @throws FilesMetadataTypeException
  235. * @since 28.0.0
  236. */
  237. public function getStringList(string $key): array {
  238. if (!array_key_exists($key, $this->metadata)) {
  239. throw new FilesMetadataNotFoundException();
  240. }
  241. return $this->metadata[$key]->getValueStringList();
  242. }
  243. /**
  244. * @param string $key metadata key
  245. *
  246. * @inheritDoc
  247. * @return int[] metadata value
  248. * @throws FilesMetadataNotFoundException
  249. * @throws FilesMetadataTypeException
  250. * @since 28.0.0
  251. */
  252. public function getIntList(string $key): array {
  253. if (!array_key_exists($key, $this->metadata)) {
  254. throw new FilesMetadataNotFoundException();
  255. }
  256. return $this->metadata[$key]->getValueIntList();
  257. }
  258. /**
  259. * @param string $key metadata key
  260. *
  261. * @inheritDoc
  262. * @return string value type
  263. * @throws FilesMetadataNotFoundException
  264. * @see IMetadataValueWrapper::TYPE_STRING
  265. * @see IMetadataValueWrapper::TYPE_INT
  266. * @see IMetadataValueWrapper::TYPE_FLOAT
  267. * @see IMetadataValueWrapper::TYPE_BOOL
  268. * @see IMetadataValueWrapper::TYPE_ARRAY
  269. * @see IMetadataValueWrapper::TYPE_STRING_LIST
  270. * @see IMetadataValueWrapper::TYPE_INT_LIST
  271. * @since 28.0.0
  272. */
  273. public function getType(string $key): string {
  274. if (!array_key_exists($key, $this->metadata)) {
  275. throw new FilesMetadataNotFoundException();
  276. }
  277. return $this->metadata[$key]->getType();
  278. }
  279. /**
  280. * @param string $key metadata key
  281. * @param string $value metadata value
  282. * @param bool $index set TRUE if value must be indexed
  283. *
  284. * @inheritDoc
  285. * @return self
  286. * @throws FilesMetadataKeyFormatException
  287. * @since 28.0.0
  288. */
  289. public function setString(string $key, string $value, bool $index = false): IFilesMetadata {
  290. $this->confirmKeyFormat($key);
  291. try {
  292. if ($this->getString($key) === $value && $index === $this->isIndex($key)) {
  293. return $this; // we ignore if value and index have not changed
  294. }
  295. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  296. // if value does not exist, or type has changed, we keep on the writing
  297. }
  298. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING);
  299. $this->updated = true;
  300. $this->metadata[$key] = $meta->setValueString($value)->setIndexed($index);
  301. return $this;
  302. }
  303. /**
  304. * @param string $key metadata key
  305. * @param int $value metadata value
  306. * @param bool $index set TRUE if value must be indexed
  307. *
  308. * @inheritDoc
  309. * @return self
  310. * @throws FilesMetadataKeyFormatException
  311. * @since 28.0.0
  312. */
  313. public function setInt(string $key, int $value, bool $index = false): IFilesMetadata {
  314. $this->confirmKeyFormat($key);
  315. try {
  316. if ($this->getInt($key) === $value && $index === $this->isIndex($key)) {
  317. return $this; // we ignore if value have not changed
  318. }
  319. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  320. // if value does not exist, or type has changed, we keep on the writing
  321. }
  322. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_INT);
  323. $this->metadata[$key] = $meta->setValueInt($value)->setIndexed($index);
  324. $this->updated = true;
  325. return $this;
  326. }
  327. /**
  328. * @param string $key metadata key
  329. * @param float $value metadata value
  330. *
  331. * @inheritDoc
  332. * @return self
  333. * @throws FilesMetadataKeyFormatException
  334. * @since 28.0.0
  335. */
  336. public function setFloat(string $key, float $value, bool $index = false): IFilesMetadata {
  337. $this->confirmKeyFormat($key);
  338. try {
  339. if ($this->getFloat($key) === $value && $index === $this->isIndex($key)) {
  340. return $this; // we ignore if value have not changed
  341. }
  342. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  343. // if value does not exist, or type has changed, we keep on the writing
  344. }
  345. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_FLOAT);
  346. $this->metadata[$key] = $meta->setValueFloat($value)->setIndexed($index);
  347. $this->updated = true;
  348. return $this;
  349. }
  350. /**
  351. * @param string $key metadata key
  352. * @param bool $value metadata value
  353. * @param bool $index set TRUE if value must be indexed
  354. *
  355. * @inheritDoc
  356. * @return self
  357. * @throws FilesMetadataKeyFormatException
  358. * @since 28.0.0
  359. */
  360. public function setBool(string $key, bool $value, bool $index = false): IFilesMetadata {
  361. $this->confirmKeyFormat($key);
  362. try {
  363. if ($this->getBool($key) === $value && $index === $this->isIndex($key)) {
  364. return $this; // we ignore if value have not changed
  365. }
  366. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  367. // if value does not exist, or type has changed, we keep on the writing
  368. }
  369. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_BOOL);
  370. $this->metadata[$key] = $meta->setValueBool($value)->setIndexed($index);
  371. $this->updated = true;
  372. return $this;
  373. }
  374. /**
  375. * @param string $key metadata key
  376. * @param array $value metadata value
  377. *
  378. * @inheritDoc
  379. * @return self
  380. * @throws FilesMetadataKeyFormatException
  381. * @since 28.0.0
  382. */
  383. public function setArray(string $key, array $value): IFilesMetadata {
  384. $this->confirmKeyFormat($key);
  385. try {
  386. if ($this->getArray($key) === $value) {
  387. return $this; // we ignore if value have not changed
  388. }
  389. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  390. // if value does not exist, or type has changed, we keep on the writing
  391. }
  392. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_ARRAY);
  393. $this->metadata[$key] = $meta->setValueArray($value);
  394. $this->updated = true;
  395. return $this;
  396. }
  397. /**
  398. * @param string $key metadata key
  399. * @param string[] $value metadata value
  400. * @param bool $index set TRUE if each values from the list must be indexed
  401. *
  402. * @inheritDoc
  403. * @return self
  404. * @throws FilesMetadataKeyFormatException
  405. * @since 28.0.0
  406. */
  407. public function setStringList(string $key, array $value, bool $index = false): IFilesMetadata {
  408. $this->confirmKeyFormat($key);
  409. try {
  410. if ($this->getStringList($key) === $value) {
  411. return $this; // we ignore if value have not changed
  412. }
  413. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  414. // if value does not exist, or type has changed, we keep on the writing
  415. }
  416. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST);
  417. $this->metadata[$key] = $meta->setValueStringList($value)->setIndexed($index);
  418. $this->updated = true;
  419. return $this;
  420. }
  421. /**
  422. * @param string $key metadata key
  423. * @param int[] $value metadata value
  424. * @param bool $index set TRUE if each values from the list must be indexed
  425. *
  426. * @inheritDoc
  427. * @return self
  428. * @throws FilesMetadataKeyFormatException
  429. * @since 28.0.0
  430. */
  431. public function setIntList(string $key, array $value, bool $index = false): IFilesMetadata {
  432. $this->confirmKeyFormat($key);
  433. try {
  434. if ($this->getIntList($key) === $value) {
  435. return $this; // we ignore if value have not changed
  436. }
  437. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  438. // if value does not exist, or type has changed, we keep on the writing
  439. }
  440. $valueWrapper = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_INT_LIST);
  441. $this->metadata[$key] = $valueWrapper->setValueIntList($value)->setIndexed($index);
  442. $this->updated = true;
  443. return $this;
  444. }
  445. /**
  446. * @param string $key metadata key
  447. *
  448. * @inheritDoc
  449. * @return self
  450. * @since 28.0.0
  451. */
  452. public function unset(string $key): IFilesMetadata {
  453. if (!array_key_exists($key, $this->metadata)) {
  454. return $this;
  455. }
  456. unset($this->metadata[$key]);
  457. $this->updated = true;
  458. return $this;
  459. }
  460. /**
  461. * @param string $keyPrefix metadata key prefix
  462. *
  463. * @inheritDoc
  464. * @return self
  465. * @since 28.0.0
  466. */
  467. public function removeStartsWith(string $keyPrefix): IFilesMetadata {
  468. if ($keyPrefix === '') {
  469. return $this;
  470. }
  471. foreach ($this->getKeys() as $key) {
  472. if (str_starts_with($key, $keyPrefix)) {
  473. $this->unset($key);
  474. }
  475. }
  476. return $this;
  477. }
  478. /**
  479. * @param string $key
  480. *
  481. * @return void
  482. * @throws FilesMetadataKeyFormatException
  483. */
  484. private function confirmKeyFormat(string $key): void {
  485. $acceptedChars = ['-', '_'];
  486. if (ctype_alnum(str_replace($acceptedChars, '', $key))) {
  487. return;
  488. }
  489. throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-, _)');
  490. }
  491. /**
  492. * @inheritDoc
  493. * @return bool TRUE if metadata have been modified
  494. * @since 28.0.0
  495. */
  496. public function updated(): bool {
  497. return $this->updated;
  498. }
  499. public function jsonSerialize(bool $emptyValues = false): array {
  500. $data = [];
  501. foreach ($this->metadata as $metaKey => $metaValueWrapper) {
  502. $data[$metaKey] = $metaValueWrapper->jsonSerialize($emptyValues);
  503. }
  504. return $data;
  505. }
  506. /**
  507. * @return array<string, string|int|bool|float|string[]|int[]>
  508. */
  509. public function asArray(): array {
  510. $data = [];
  511. foreach ($this->metadata as $metaKey => $metaValueWrapper) {
  512. try {
  513. $data[$metaKey] = $metaValueWrapper->getValueAny();
  514. } catch (FilesMetadataNotFoundException $e) {
  515. // ignore exception
  516. }
  517. }
  518. return $data;
  519. }
  520. /**
  521. * @param array $data
  522. *
  523. * @inheritDoc
  524. * @return IFilesMetadata
  525. * @since 28.0.0
  526. */
  527. public function import(array $data): IFilesMetadata {
  528. foreach ($data as $k => $v) {
  529. $valueWrapper = new MetadataValueWrapper();
  530. $this->metadata[$k] = $valueWrapper->import($v);
  531. }
  532. $this->updated = false;
  533. return $this;
  534. }
  535. /**
  536. * import data from database to configure this model
  537. *
  538. * @param array $data
  539. * @param string $prefix
  540. *
  541. * @return IFilesMetadata
  542. * @throws FilesMetadataNotFoundException
  543. * @since 28.0.0
  544. */
  545. public function importFromDatabase(array $data, string $prefix = ''): IFilesMetadata {
  546. try {
  547. $this->syncToken = $data[$prefix . 'sync_token'] ?? '';
  548. return $this->import(
  549. json_decode(
  550. $data[$prefix . 'json'] ?? '[]',
  551. true,
  552. 512,
  553. JSON_THROW_ON_ERROR
  554. )
  555. );
  556. } catch (JsonException) {
  557. throw new FilesMetadataNotFoundException();
  558. }
  559. }
  560. }