FilesMetadata.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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. /**
  142. * @param string $key metadata key
  143. *
  144. * @inheritDoc
  145. * @return string metadata value
  146. * @throws FilesMetadataNotFoundException
  147. * @throws FilesMetadataTypeException
  148. * @since 28.0.0
  149. */
  150. public function getString(string $key): string {
  151. if (!array_key_exists($key, $this->metadata)) {
  152. throw new FilesMetadataNotFoundException();
  153. }
  154. return $this->metadata[$key]->getValueString();
  155. }
  156. /**
  157. * @param string $key metadata key
  158. *
  159. * @inheritDoc
  160. * @return int metadata value
  161. * @throws FilesMetadataNotFoundException
  162. * @throws FilesMetadataTypeException
  163. * @since 28.0.0
  164. */
  165. public function getInt(string $key): int {
  166. if (!array_key_exists($key, $this->metadata)) {
  167. throw new FilesMetadataNotFoundException();
  168. }
  169. return $this->metadata[$key]->getValueInt();
  170. }
  171. /**
  172. * @param string $key metadata key
  173. *
  174. * @inheritDoc
  175. * @return float metadata value
  176. * @throws FilesMetadataNotFoundException
  177. * @throws FilesMetadataTypeException
  178. * @since 28.0.0
  179. */
  180. public function getFloat(string $key): float {
  181. if (!array_key_exists($key, $this->metadata)) {
  182. throw new FilesMetadataNotFoundException();
  183. }
  184. return $this->metadata[$key]->getValueFloat();
  185. }
  186. /**
  187. * @param string $key metadata key
  188. *
  189. * @inheritDoc
  190. * @return bool metadata value
  191. * @throws FilesMetadataNotFoundException
  192. * @throws FilesMetadataTypeException
  193. * @since 28.0.0
  194. */
  195. public function getBool(string $key): bool {
  196. if (!array_key_exists($key, $this->metadata)) {
  197. throw new FilesMetadataNotFoundException();
  198. }
  199. return $this->metadata[$key]->getValueBool();
  200. }
  201. /**
  202. * @param string $key metadata key
  203. *
  204. * @inheritDoc
  205. * @return array metadata value
  206. * @throws FilesMetadataNotFoundException
  207. * @throws FilesMetadataTypeException
  208. * @since 28.0.0
  209. */
  210. public function getArray(string $key): array {
  211. if (!array_key_exists($key, $this->metadata)) {
  212. throw new FilesMetadataNotFoundException();
  213. }
  214. return $this->metadata[$key]->getValueArray();
  215. }
  216. /**
  217. * @param string $key metadata key
  218. *
  219. * @inheritDoc
  220. * @return string[] metadata value
  221. * @throws FilesMetadataNotFoundException
  222. * @throws FilesMetadataTypeException
  223. * @since 28.0.0
  224. */
  225. public function getStringList(string $key): array {
  226. if (!array_key_exists($key, $this->metadata)) {
  227. throw new FilesMetadataNotFoundException();
  228. }
  229. return $this->metadata[$key]->getValueStringList();
  230. }
  231. /**
  232. * @param string $key metadata key
  233. *
  234. * @inheritDoc
  235. * @return int[] metadata value
  236. * @throws FilesMetadataNotFoundException
  237. * @throws FilesMetadataTypeException
  238. * @since 28.0.0
  239. */
  240. public function getIntList(string $key): array {
  241. if (!array_key_exists($key, $this->metadata)) {
  242. throw new FilesMetadataNotFoundException();
  243. }
  244. return $this->metadata[$key]->getValueIntList();
  245. }
  246. /**
  247. * @param string $key metadata key
  248. *
  249. * @inheritDoc
  250. * @return string value type
  251. * @throws FilesMetadataNotFoundException
  252. * @see IMetadataValueWrapper::TYPE_STRING
  253. * @see IMetadataValueWrapper::TYPE_INT
  254. * @see IMetadataValueWrapper::TYPE_FLOAT
  255. * @see IMetadataValueWrapper::TYPE_BOOL
  256. * @see IMetadataValueWrapper::TYPE_ARRAY
  257. * @see IMetadataValueWrapper::TYPE_STRING_LIST
  258. * @see IMetadataValueWrapper::TYPE_INT_LIST
  259. * @since 28.0.0
  260. */
  261. public function getType(string $key): string {
  262. if (!array_key_exists($key, $this->metadata)) {
  263. throw new FilesMetadataNotFoundException();
  264. }
  265. return $this->metadata[$key]->getType();
  266. }
  267. /**
  268. * @param string $key metadata key
  269. * @param string $value metadata value
  270. * @param bool $index set TRUE if value must be indexed
  271. *
  272. * @inheritDoc
  273. * @return self
  274. * @throws FilesMetadataKeyFormatException
  275. * @since 28.0.0
  276. */
  277. public function setString(string $key, string $value, bool $index = false): IFilesMetadata {
  278. $this->confirmKeyFormat($key);
  279. try {
  280. if ($this->getString($key) === $value && $index === $this->isIndex($key)) {
  281. return $this; // we ignore if value and index have not changed
  282. }
  283. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  284. // if value does not exist, or type has changed, we keep on the writing
  285. }
  286. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING);
  287. $this->updated = true;
  288. $this->metadata[$key] = $meta->setValueString($value)->setIndexed($index);
  289. return $this;
  290. }
  291. /**
  292. * @param string $key metadata key
  293. * @param int $value metadata value
  294. * @param bool $index set TRUE if value must be indexed
  295. *
  296. * @inheritDoc
  297. * @return self
  298. * @throws FilesMetadataKeyFormatException
  299. * @since 28.0.0
  300. */
  301. public function setInt(string $key, int $value, bool $index = false): IFilesMetadata {
  302. $this->confirmKeyFormat($key);
  303. try {
  304. if ($this->getInt($key) === $value && $index === $this->isIndex($key)) {
  305. return $this; // we ignore if value have not changed
  306. }
  307. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  308. // if value does not exist, or type has changed, we keep on the writing
  309. }
  310. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_INT);
  311. $this->metadata[$key] = $meta->setValueInt($value)->setIndexed($index);
  312. $this->updated = true;
  313. return $this;
  314. }
  315. /**
  316. * @param string $key metadata key
  317. * @param float $value metadata value
  318. *
  319. * @inheritDoc
  320. * @return self
  321. * @throws FilesMetadataKeyFormatException
  322. * @since 28.0.0
  323. */
  324. public function setFloat(string $key, float $value, bool $index = false): IFilesMetadata {
  325. $this->confirmKeyFormat($key);
  326. try {
  327. if ($this->getFloat($key) === $value && $index === $this->isIndex($key)) {
  328. return $this; // we ignore if value have not changed
  329. }
  330. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  331. // if value does not exist, or type has changed, we keep on the writing
  332. }
  333. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_FLOAT);
  334. $this->metadata[$key] = $meta->setValueFloat($value)->setIndexed($index);
  335. $this->updated = true;
  336. return $this;
  337. }
  338. /**
  339. * @param string $key metadata key
  340. * @param bool $value metadata value
  341. * @param bool $index set TRUE if value must be indexed
  342. *
  343. * @inheritDoc
  344. * @return self
  345. * @throws FilesMetadataKeyFormatException
  346. * @since 28.0.0
  347. */
  348. public function setBool(string $key, bool $value, bool $index = false): IFilesMetadata {
  349. $this->confirmKeyFormat($key);
  350. try {
  351. if ($this->getBool($key) === $value && $index === $this->isIndex($key)) {
  352. return $this; // we ignore if value have not changed
  353. }
  354. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  355. // if value does not exist, or type has changed, we keep on the writing
  356. }
  357. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_BOOL);
  358. $this->metadata[$key] = $meta->setValueBool($value)->setIndexed($index);
  359. $this->updated = true;
  360. return $this;
  361. }
  362. /**
  363. * @param string $key metadata key
  364. * @param array $value metadata value
  365. *
  366. * @inheritDoc
  367. * @return self
  368. * @throws FilesMetadataKeyFormatException
  369. * @since 28.0.0
  370. */
  371. public function setArray(string $key, array $value): IFilesMetadata {
  372. $this->confirmKeyFormat($key);
  373. try {
  374. if ($this->getArray($key) === $value) {
  375. return $this; // we ignore if value have not changed
  376. }
  377. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  378. // if value does not exist, or type has changed, we keep on the writing
  379. }
  380. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_ARRAY);
  381. $this->metadata[$key] = $meta->setValueArray($value);
  382. $this->updated = true;
  383. return $this;
  384. }
  385. /**
  386. * @param string $key metadata key
  387. * @param string[] $value metadata value
  388. * @param bool $index set TRUE if each values from the list must be indexed
  389. *
  390. * @inheritDoc
  391. * @return self
  392. * @throws FilesMetadataKeyFormatException
  393. * @since 28.0.0
  394. */
  395. public function setStringList(string $key, array $value, bool $index = false): IFilesMetadata {
  396. $this->confirmKeyFormat($key);
  397. try {
  398. if ($this->getStringList($key) === $value) {
  399. return $this; // we ignore if value have not changed
  400. }
  401. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  402. // if value does not exist, or type has changed, we keep on the writing
  403. }
  404. $meta = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST);
  405. $this->metadata[$key] = $meta->setValueStringList($value)->setIndexed($index);
  406. $this->updated = true;
  407. return $this;
  408. }
  409. /**
  410. * @param string $key metadata key
  411. * @param int[] $value metadata value
  412. * @param bool $index set TRUE if each values from the list must be indexed
  413. *
  414. * @inheritDoc
  415. * @return self
  416. * @throws FilesMetadataKeyFormatException
  417. * @since 28.0.0
  418. */
  419. public function setIntList(string $key, array $value, bool $index = false): IFilesMetadata {
  420. $this->confirmKeyFormat($key);
  421. try {
  422. if ($this->getIntList($key) === $value) {
  423. return $this; // we ignore if value have not changed
  424. }
  425. } catch (FilesMetadataNotFoundException|FilesMetadataTypeException $e) {
  426. // if value does not exist, or type has changed, we keep on the writing
  427. }
  428. $valueWrapper = new MetadataValueWrapper(IMetadataValueWrapper::TYPE_STRING_LIST);
  429. $this->metadata[$key] = $valueWrapper->setValueIntList($value)->setIndexed($index);
  430. $this->updated = true;
  431. return $this;
  432. }
  433. /**
  434. * @param string $key metadata key
  435. *
  436. * @inheritDoc
  437. * @return self
  438. * @since 28.0.0
  439. */
  440. public function unset(string $key): IFilesMetadata {
  441. if (!array_key_exists($key, $this->metadata)) {
  442. return $this;
  443. }
  444. unset($this->metadata[$key]);
  445. $this->updated = true;
  446. return $this;
  447. }
  448. /**
  449. * @param string $keyPrefix metadata key prefix
  450. *
  451. * @inheritDoc
  452. * @return self
  453. * @since 28.0.0
  454. */
  455. public function removeStartsWith(string $keyPrefix): IFilesMetadata {
  456. if ($keyPrefix === '') {
  457. return $this;
  458. }
  459. foreach ($this->getKeys() as $key) {
  460. if (str_starts_with($key, $keyPrefix)) {
  461. $this->unset($key);
  462. }
  463. }
  464. return $this;
  465. }
  466. /**
  467. * @param string $key
  468. *
  469. * @return void
  470. * @throws FilesMetadataKeyFormatException
  471. */
  472. private function confirmKeyFormat(string $key): void {
  473. $acceptedChars = ['-', '_'];
  474. if (ctype_alnum(str_replace($acceptedChars, '', $key))) {
  475. return;
  476. }
  477. throw new FilesMetadataKeyFormatException('key can only contains alphanumerical characters, and dash (-, _)');
  478. }
  479. /**
  480. * @inheritDoc
  481. * @return bool TRUE if metadata have been modified
  482. * @since 28.0.0
  483. */
  484. public function updated(): bool {
  485. return $this->updated;
  486. }
  487. public function jsonSerialize(bool $emptyValues = false): array {
  488. $data = [];
  489. foreach ($this->metadata as $metaKey => $metaValueWrapper) {
  490. $data[$metaKey] = $metaValueWrapper->jsonSerialize($emptyValues);
  491. }
  492. return $data;
  493. }
  494. /**
  495. * @return array<string, string|int|bool|float|string[]|int[]>
  496. */
  497. public function asArray(): array {
  498. $data = [];
  499. foreach ($this->metadata as $metaKey => $metaValueWrapper) {
  500. try {
  501. $data[$metaKey] = $metaValueWrapper->getValueAny();
  502. } catch (FilesMetadataNotFoundException $e) {
  503. // ignore exception
  504. }
  505. }
  506. return $data;
  507. }
  508. /**
  509. * @param array $data
  510. *
  511. * @inheritDoc
  512. * @return IFilesMetadata
  513. * @since 28.0.0
  514. */
  515. public function import(array $data): IFilesMetadata {
  516. foreach ($data as $k => $v) {
  517. $valueWrapper = new MetadataValueWrapper();
  518. $this->metadata[$k] = $valueWrapper->import($v);
  519. }
  520. $this->updated = false;
  521. return $this;
  522. }
  523. /**
  524. * import data from database to configure this model
  525. *
  526. * @param array $data
  527. * @param string $prefix
  528. *
  529. * @return IFilesMetadata
  530. * @throws FilesMetadataNotFoundException
  531. * @since 28.0.0
  532. */
  533. public function importFromDatabase(array $data, string $prefix = ''): IFilesMetadata {
  534. try {
  535. $this->syncToken = $data[$prefix . 'sync_token'] ?? '';
  536. return $this->import(
  537. json_decode(
  538. $data[$prefix . 'json'] ?? '[]',
  539. true,
  540. 512,
  541. JSON_THROW_ON_ERROR
  542. )
  543. );
  544. } catch (JsonException) {
  545. throw new FilesMetadataNotFoundException();
  546. }
  547. }
  548. }