diff --git a/appinfo/database.xml b/appinfo/database.xml new file mode 100644 index 000000000..daf4f51da --- /dev/null +++ b/appinfo/database.xml @@ -0,0 +1,59 @@ + + + *dbname* + true + utf8 + + *dbprefix*notes_meta + + + id + integer + true + true + + + file_id + integer + true + + + user_id + text + true + 64 + + + last_update + integer + true + + + etag + text + true + 32 + + + notes_meta_file_id_index + + file_id + + + + notes_meta_user_id_index + + user_id + + + + notes_meta_file_id_user_id_index + true + + file_id + user_id + + + +
+
diff --git a/controller/notesapicontroller.php b/controller/notesapicontroller.php index 4f55b3f93..bcca536c6 100644 --- a/controller/notesapicontroller.php +++ b/controller/notesapicontroller.php @@ -12,10 +12,12 @@ namespace OCA\Notes\Controller; use OCP\AppFramework\ApiController; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; use OCA\Notes\Service\NotesService; +use OCA\Notes\Service\MetaService; use OCA\Notes\Db\Note; /** @@ -29,6 +31,8 @@ class NotesApiController extends ApiController { /** @var NotesService */ private $service; + /** @var MetaService */ + private $metaService; /** @var string */ private $userId; @@ -38,10 +42,10 @@ class NotesApiController extends ApiController { * @param NotesService $service * @param string $UserId */ - public function __construct($AppName, IRequest $request, - NotesService $service, $UserId){ + public function __construct($AppName, IRequest $request, NotesService $service, MetaService $metaService, $UserId) { parent::__construct($AppName, $request); $this->service = $service; + $this->metaService = $metaService; $this->userId = $UserId; } @@ -52,7 +56,7 @@ public function __construct($AppName, IRequest $request, * notes * @return Note */ - private function excludeFields(Note $note, array $exclude) { + private function excludeFields(Note &$note, array $exclude) { if(count($exclude) > 0) { foreach ($exclude as $field) { if(property_exists($note, $field)) { @@ -72,13 +76,28 @@ private function excludeFields(Note $note, array $exclude) { * @param string $exclude * @return DataResponse */ - public function index($exclude='') { + public function index($exclude='', $pruneBefore=0) { $exclude = explode(',', $exclude); + $now = new \DateTime(); // this must be before loading notes if there are concurrent changes possible $notes = $this->service->getAll($this->userId); + $metas = $this->metaService->updateAll($this->userId, $notes); foreach ($notes as $note) { - $note = $this->excludeFields($note, $exclude); + $lastUpdate = $metas[$note->getId()]->getLastUpdate(); + if($pruneBefore && $lastUpdate<$pruneBefore) { + $vars = get_object_vars($note); + unset($vars['id']); + $this->excludeFields($note, array_keys($vars)); + } else { + $this->excludeFields($note, $exclude); + } + } + $etag = md5(json_encode($notes)); + if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') { + return new DataResponse([], Http::STATUS_NOT_MODIFIED); } - return new DataResponse($notes); + return (new DataResponse($notes)) + ->setLastModified($now) + ->setETag($etag); } diff --git a/db/meta.php b/db/meta.php new file mode 100644 index 000000000..44e34d00d --- /dev/null +++ b/db/meta.php @@ -0,0 +1,32 @@ +setUserId($userId); + $meta->setFileId($note->getId()); + $meta->setLastUpdate(time()); + $meta->setEtag($note->getEtag()); + return $meta; + } +} diff --git a/db/metamapper.php b/db/metamapper.php new file mode 100644 index 000000000..14d789eaf --- /dev/null +++ b/db/metamapper.php @@ -0,0 +1,29 @@ +findEntities($sql, [$userId]); + } + + public function get($userId, $fileId) { + $sql = 'SELECT * FROM `*PREFIX*notes_meta` WHERE user_id=? AND file_id=?'; + return $this->findEntity($sql, [$userId, $fileId]); + } +} diff --git a/db/note.php b/db/note.php index 31f32016b..f3536ce17 100644 --- a/db/note.php +++ b/db/note.php @@ -19,6 +19,8 @@ * Class Note * @method integer getId() * @method void setId(integer $value) + * @method string getEtag() + * @method void setEtag(string $value) * @method integer getModified() * @method void setModified(integer $value) * @method string getTitle() @@ -33,6 +35,7 @@ */ class Note extends Entity { + public $etag; public $modified; public $title; public $category; @@ -60,6 +63,7 @@ public static function fromFile(File $file, Folder $notesFolder, $tags=[]){ $note->setFavorite(true); //unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]); } + $note->updateETag(); $note->resetUpdatedFields(); return $note; } @@ -70,4 +74,16 @@ private static function convertEncoding($str) { } return $str; } + + private function updateETag() { + // collect all relevant attributes + $data = ''; + foreach(get_object_vars($this) as $key => $val) { + if($key!=='etag') { + $data .= $val; + } + } + $etag = md5($data); + $this->setEtag($etag); + } } diff --git a/service/metaservice.php b/service/metaservice.php new file mode 100644 index 000000000..1b5f47e11 --- /dev/null +++ b/service/metaservice.php @@ -0,0 +1,69 @@ +metaMapper = $metaMapper; + } + + public function updateAll($userId, Array $notes) { + $metas = $this->metaMapper->getAll($userId); + $metas = $this->getIndexedArray($metas, 'fileId'); + $notes = $this->getIndexedArray($notes, 'id'); + foreach($metas as $id=>$meta) { + if(!array_key_exists($id, $notes)) { + // DELETE obsolete notes + $this->metaMapper->delete($meta); + unset($metas[$id]); + } + } + foreach($notes as $id=>$note) { + if(!array_key_exists($id, $metas)) { + // INSERT new notes + $metas[$note->getId()] = $this->create($userId, $note); + } elseif($note->getEtag()!==$metas[$id]->getEtag()) { + // UPDATE changed notes + $meta = $metas[$id]; + $this->updateIfNeeded($meta, $note); + } + } + return $metas; + } + + private function getIndexedArray(array $data, $property) { + $property = ucfirst($property); + $getter = 'get'.$property; + $result = array(); + foreach($data as $entity) { + $result[$entity->$getter()] = $entity; + } + return $result; + } + + private function updateIfNeeded(&$meta, $note) { + if($note->getEtag()!==$meta->getEtag()) { + $meta->setEtag($note->getEtag()); + $meta->setLastUpdate(time()); + $this->metaMapper->update($meta); + } + } +} diff --git a/tests/unit/controller/NotesApiControllerTest.php b/tests/unit/controller/NotesApiControllerTest.php index 3ce8cab6d..3f3feeab9 100644 --- a/tests/unit/controller/NotesApiControllerTest.php +++ b/tests/unit/controller/NotesApiControllerTest.php @@ -87,12 +87,14 @@ public function testGetAllHide(){ $this->assertEquals(json_encode([ [ + 'etag' => null, 'modified' => 123, 'category' => null, 'favorite' => false, 'id' => 3, ], [ + 'etag' => null, 'modified' => 111, 'category' => null, 'favorite' => false, @@ -139,6 +141,7 @@ public function testGetHide(){ $response = $this->controller->get(3, 'title,content'); $this->assertEquals(json_encode([ + 'etag' => null, 'modified' => 123, 'category' => null, 'favorite' => false,