Merge pull request #95 from nextcloud/api-speed-up
API speed-up (ETag, Last-Modified)
This commit is contained in:
commit
6df9129567
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<database>
|
||||||
|
<name>*dbname*</name>
|
||||||
|
<create>true</create>
|
||||||
|
<charset>utf8</charset>
|
||||||
|
<table>
|
||||||
|
<name>*dbprefix*notes_meta</name>
|
||||||
|
<declaration>
|
||||||
|
<field>
|
||||||
|
<name>id</name>
|
||||||
|
<type>integer</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
<autoincrement>true</autoincrement>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
<type>integer</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>user_id</name>
|
||||||
|
<type>text</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
<length>64</length>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>last_update</name>
|
||||||
|
<type>integer</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<name>etag</name>
|
||||||
|
<type>text</type>
|
||||||
|
<notnull>true</notnull>
|
||||||
|
<length>32</length>
|
||||||
|
</field>
|
||||||
|
<index>
|
||||||
|
<name>notes_meta_file_id_index</name>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
</field>
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<name>notes_meta_user_id_index</name>
|
||||||
|
<field>
|
||||||
|
<name>user_id</name>
|
||||||
|
</field>
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<name>notes_meta_file_id_user_id_index</name>
|
||||||
|
<unique>true</unique>
|
||||||
|
<field>
|
||||||
|
<name>file_id</name>
|
||||||
|
<name>user_id</name>
|
||||||
|
</field>
|
||||||
|
</index>
|
||||||
|
</declaration>
|
||||||
|
</table>
|
||||||
|
</database>
|
|
@ -12,10 +12,12 @@
|
||||||
namespace OCA\Notes\Controller;
|
namespace OCA\Notes\Controller;
|
||||||
|
|
||||||
use OCP\AppFramework\ApiController;
|
use OCP\AppFramework\ApiController;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\DataResponse;
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
|
|
||||||
use OCA\Notes\Service\NotesService;
|
use OCA\Notes\Service\NotesService;
|
||||||
|
use OCA\Notes\Service\MetaService;
|
||||||
use OCA\Notes\Db\Note;
|
use OCA\Notes\Db\Note;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +31,8 @@ class NotesApiController extends ApiController {
|
||||||
|
|
||||||
/** @var NotesService */
|
/** @var NotesService */
|
||||||
private $service;
|
private $service;
|
||||||
|
/** @var MetaService */
|
||||||
|
private $metaService;
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $userId;
|
private $userId;
|
||||||
|
|
||||||
|
@ -38,10 +42,10 @@ class NotesApiController extends ApiController {
|
||||||
* @param NotesService $service
|
* @param NotesService $service
|
||||||
* @param string $UserId
|
* @param string $UserId
|
||||||
*/
|
*/
|
||||||
public function __construct($AppName, IRequest $request,
|
public function __construct($AppName, IRequest $request, NotesService $service, MetaService $metaService, $UserId) {
|
||||||
NotesService $service, $UserId){
|
|
||||||
parent::__construct($AppName, $request);
|
parent::__construct($AppName, $request);
|
||||||
$this->service = $service;
|
$this->service = $service;
|
||||||
|
$this->metaService = $metaService;
|
||||||
$this->userId = $UserId;
|
$this->userId = $UserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +56,7 @@ class NotesApiController extends ApiController {
|
||||||
* notes
|
* notes
|
||||||
* @return Note
|
* @return Note
|
||||||
*/
|
*/
|
||||||
private function excludeFields(Note $note, array $exclude) {
|
private function excludeFields(Note &$note, array $exclude) {
|
||||||
if(count($exclude) > 0) {
|
if(count($exclude) > 0) {
|
||||||
foreach ($exclude as $field) {
|
foreach ($exclude as $field) {
|
||||||
if(property_exists($note, $field)) {
|
if(property_exists($note, $field)) {
|
||||||
|
@ -72,13 +76,28 @@ class NotesApiController extends ApiController {
|
||||||
* @param string $exclude
|
* @param string $exclude
|
||||||
* @return DataResponse
|
* @return DataResponse
|
||||||
*/
|
*/
|
||||||
public function index($exclude='') {
|
public function index($exclude='', $pruneBefore=0) {
|
||||||
$exclude = explode(',', $exclude);
|
$exclude = explode(',', $exclude);
|
||||||
|
$now = new \DateTime(); // this must be before loading notes if there are concurrent changes possible
|
||||||
$notes = $this->service->getAll($this->userId);
|
$notes = $this->service->getAll($this->userId);
|
||||||
|
$metas = $this->metaService->updateAll($this->userId, $notes);
|
||||||
foreach ($notes as $note) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new DataResponse($notes);
|
$etag = md5(json_encode($notes));
|
||||||
|
if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_MODIFIED);
|
||||||
|
}
|
||||||
|
return (new DataResponse($notes))
|
||||||
|
->setLastModified($now)
|
||||||
|
->setETag($etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nextcloud - Notes
|
||||||
|
*
|
||||||
|
* This file is licensed under the Affero General Public License version 3 or
|
||||||
|
* later. See the COPYING file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Notes\Db;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Db\Entity;
|
||||||
|
|
||||||
|
class Meta extends Entity {
|
||||||
|
|
||||||
|
public $userId;
|
||||||
|
public $fileId;
|
||||||
|
public $lastUpdate;
|
||||||
|
public $etag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Note $note
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromNote(Note $note, $userId) {
|
||||||
|
$meta = new static();
|
||||||
|
$meta->setUserId($userId);
|
||||||
|
$meta->setFileId($note->getId());
|
||||||
|
$meta->setLastUpdate(time());
|
||||||
|
$meta->setEtag($note->getEtag());
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nextcloud - Notes
|
||||||
|
*
|
||||||
|
* This file is licensed under the Affero General Public License version 3 or
|
||||||
|
* later. See the COPYING file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Notes\Db;
|
||||||
|
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
use OCP\AppFramework\Db\Mapper;
|
||||||
|
|
||||||
|
class MetaMapper extends Mapper {
|
||||||
|
|
||||||
|
public function __construct(IDBConnection $db) {
|
||||||
|
parent::__construct($db, 'notes_meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll($userId) {
|
||||||
|
$sql = 'SELECT * FROM `*PREFIX*notes_meta` WHERE user_id=?';
|
||||||
|
return $this->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]);
|
||||||
|
}
|
||||||
|
}
|
16
db/note.php
16
db/note.php
|
@ -19,6 +19,8 @@ use OCP\AppFramework\Db\Entity;
|
||||||
* Class Note
|
* Class Note
|
||||||
* @method integer getId()
|
* @method integer getId()
|
||||||
* @method void setId(integer $value)
|
* @method void setId(integer $value)
|
||||||
|
* @method string getEtag()
|
||||||
|
* @method void setEtag(string $value)
|
||||||
* @method integer getModified()
|
* @method integer getModified()
|
||||||
* @method void setModified(integer $value)
|
* @method void setModified(integer $value)
|
||||||
* @method string getTitle()
|
* @method string getTitle()
|
||||||
|
@ -33,6 +35,7 @@ use OCP\AppFramework\Db\Entity;
|
||||||
*/
|
*/
|
||||||
class Note extends Entity {
|
class Note extends Entity {
|
||||||
|
|
||||||
|
public $etag;
|
||||||
public $modified;
|
public $modified;
|
||||||
public $title;
|
public $title;
|
||||||
public $category;
|
public $category;
|
||||||
|
@ -60,6 +63,7 @@ class Note extends Entity {
|
||||||
$note->setFavorite(true);
|
$note->setFavorite(true);
|
||||||
//unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]);
|
//unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]);
|
||||||
}
|
}
|
||||||
|
$note->updateETag();
|
||||||
$note->resetUpdatedFields();
|
$note->resetUpdatedFields();
|
||||||
return $note;
|
return $note;
|
||||||
}
|
}
|
||||||
|
@ -70,4 +74,16 @@ class Note extends Entity {
|
||||||
}
|
}
|
||||||
return $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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nextcloud - Notes
|
||||||
|
*
|
||||||
|
* This file is licensed under the Affero General Public License version 3 or
|
||||||
|
* later. See the COPYING file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace OCA\Notes\Service;
|
||||||
|
|
||||||
|
use OCA\Notes\Db\Note;
|
||||||
|
use OCA\Notes\Db\Meta;
|
||||||
|
use OCA\Notes\Db\MetaMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MetaService
|
||||||
|
*
|
||||||
|
* @package OCA\Notes\Service
|
||||||
|
*/
|
||||||
|
class MetaService {
|
||||||
|
|
||||||
|
private $metaMapper;
|
||||||
|
|
||||||
|
public function __construct(MetaMapper $metaMapper) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,12 +87,14 @@ class NotesApiControllerTest extends PHPUnit_Framework_TestCase {
|
||||||
|
|
||||||
$this->assertEquals(json_encode([
|
$this->assertEquals(json_encode([
|
||||||
[
|
[
|
||||||
|
'etag' => null,
|
||||||
'modified' => 123,
|
'modified' => 123,
|
||||||
'category' => null,
|
'category' => null,
|
||||||
'favorite' => false,
|
'favorite' => false,
|
||||||
'id' => 3,
|
'id' => 3,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
'etag' => null,
|
||||||
'modified' => 111,
|
'modified' => 111,
|
||||||
'category' => null,
|
'category' => null,
|
||||||
'favorite' => false,
|
'favorite' => false,
|
||||||
|
@ -139,6 +141,7 @@ class NotesApiControllerTest extends PHPUnit_Framework_TestCase {
|
||||||
$response = $this->controller->get(3, 'title,content');
|
$response = $this->controller->get(3, 'title,content');
|
||||||
|
|
||||||
$this->assertEquals(json_encode([
|
$this->assertEquals(json_encode([
|
||||||
|
'etag' => null,
|
||||||
'modified' => 123,
|
'modified' => 123,
|
||||||
'category' => null,
|
'category' => null,
|
||||||
'favorite' => false,
|
'favorite' => false,
|
||||||
|
|
Loading…
Reference in New Issue