metaMapper = $metaMapper; $this->util = $util; } public function deleteByNote(int $id) : void { $this->metaMapper->deleteByNote($id); } public function getAll(string $userId, array $notes, bool $forceUpdate = false) : array { // load data $metas = $this->metaMapper->getAll($userId); $metas = $this->getIndexedArray($metas, 'fileId'); $result = []; // delete obsolete notes foreach ($metas as $id => $meta) { if (!array_key_exists($id, $notes)) { // DELETE obsolete notes $this->metaMapper->delete($meta); } } $insertErrorCount = 0; // insert/update changes foreach ($notes as $id => $note) { if (!array_key_exists($id, $metas)) { // INSERT new notes $meta = $this->createMeta($userId, $note, function () use (&$insertErrorCount) { $insertErrorCount++; }); } else { // UPDATE changed notes $meta = $metas[$id]; if ($this->updateIfNeeded($meta, $note, $forceUpdate)) { $this->metaMapper->update($meta); } } $result[$id] = new MetaNote($note, $meta); } if ($insertErrorCount) { if ($insertErrorCount == count($notes)) { $this->util->logger->warning( 'Database failed inserting Meta objects for all ' . $insertErrorCount . ' notes. ' . 'If this happens consistently, there is a problem with your database.', ); } else { $this->util->logger->warning( 'Database failed inserting Meta objects for ' . $insertErrorCount . ' times.', ); } } return $result; } public function update(string $userId, Note $note) : Meta { $meta = null; try { $meta = $this->metaMapper->findById($userId, $note->getId()); } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { } if ($meta === null) { $meta = $this->createMeta($userId, $note); } elseif ($this->updateIfNeeded($meta, $note, true)) { $this->metaMapper->update($meta); } return $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, ?callable $onError = null) : Meta { $meta = new Meta(); $meta->setUserId($userId); $meta->setFileId($note->getId()); $meta->setLastUpdate(time()); $this->updateIfNeeded($meta, $note, true); try { $this->metaMapper->insert($meta); } catch (\Throwable $e) { // It's likely that a concurrent request created this entry, too. // We can ignore this, since the result should be the same. // But we log it for being able to detect other problems. // (If this happens often, this may cause performance problems.) $loglevel = 'warning'; if ($onError) { $onError(); $loglevel = 'debug'; } $this->util->logger->$loglevel( 'Could not insert meta data for note ' . $note->getId(), [ 'exception' => $e ] ); } return $meta; } private function updateIfNeeded(Meta &$meta, Note $note, bool $forceUpdate) : bool { $generateContentEtag = $forceUpdate || !$meta->getContentEtag(); $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 { try { return Util::retryIfLocked(function () use ($note) { return md5($note->getContent()); }, 3); } catch (\Throwable $t) { $this->util->logger->error( 'Could not generate Content Etag for note ' . $note->getId(), [ 'exception' => $t ] ); return ''; } } // 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(), $note->getReadOnly(), $meta->getContentEtag(), ]; return md5(json_encode($data)); } }