metaMapper = $metaMapper; } public function deleteByNote(int $id) : void { $this->metaMapper->deleteByNote($id); } public function updateAll(string $userId, array $notes, bool $forceUpdate = false) : array { // load data $metas = $this->metaMapper->getAll($userId); $metas = $this->getIndexedArray($metas, 'fileId'); $notes = $this->getIndexedArray($notes, 'id'); // delete obsolete notes foreach ($metas as $id => $meta) { if (!array_key_exists($id, $notes)) { // DELETE obsolete notes $this->metaMapper->delete($meta); unset($metas[$id]); } } // insert/update changes foreach ($notes as $id => $note) { if (!array_key_exists($id, $metas)) { // INSERT new notes $metas[$note->getId()] = $this->createMeta($userId, $note); } else { // UPDATE changed notes $meta = $metas[$id]; if ($this->updateIfNeeded($meta, $note, $forceUpdate)) { $this->metaMapper->update($meta); } } } return $metas; } public function update(string $userId, Note $note) : void { $meta = null; try { $meta = $this->metaMapper->findById($userId, $note->getId()); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { } if ($meta === null) { $this->createMeta($userId, $note); } elseif ($this->updateIfNeeded($meta, $note, true)) { $this->metaMapper->update($meta); } } private function getIndexedArray(array $data, string $property) : array { $property = ucfirst($property); $getter = 'get'.$property; $result = []; foreach ($data as $entity) { $result[$entity->$getter()] = $entity; } return $result; } private function createMeta(string $userId, Note $note) : Meta { $meta = new Meta(); $meta->setUserId($userId); $meta->setFileId($note->getId()); $meta->setLastUpdate(time()); $this->updateIfNeeded($meta, $note, true); try { $this->metaMapper->insert($meta); /* @phan-suppress-next-line PhanUndeclaredClassCatch */ } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) { // It's likely that a concurrent request created this entry, too. // We can ignore this, since the result should be the same. } return $meta; } private function updateIfNeeded(Meta &$meta, Note $note, bool $forceUpdate) : bool { $generateContentEtag = $forceUpdate; $fileEtag = $note->getFileEtag(); // a changed File-ETag is an indicator for changed content if ($fileEtag !== $meta->getFileEtag()) { $meta->setFileEtag($fileEtag); $generateContentEtag = true; } // generate new Content-ETag if ($generateContentEtag) { $contentEtag = $this->generateContentEtag($note); // this is expensive if ($contentEtag !== $meta->getContentEtag()) { $meta->setContentEtag($contentEtag); } } // always update ETag based on meta data (not content!) $etag = $this->generateEtag($meta, $note); if ($etag !== $meta->getEtag()) { $meta->setEtag($etag); $meta->setLastUpdate(time()); } return !empty($meta->getUpdatedFields()); } // warning: this is expensive private function generateContentEtag(Note $note) : string { return md5($note->getContent()); } // this is not expensive, since we use the content ETag instead of the content itself private function generateEtag(Meta &$meta, Note $note) : string { $data = [ $note->getId(), $note->getTitle(), $note->getModified(), $note->getCategory(), $note->getFavorite(), $meta->getContentEtag(), ]; return md5(json_encode($data)); } }