# HG changeset patch # User Goffi # Date 1583835098 -3600 # Node ID c21f31355ab9fe8774bee92a88baba6218a361bc # Parent f124ed5ea78b715e37420530092ff2bcc76b6e83 configuration: "max_items" option: "max_items" is implemented using a text-single field, as it is done in the XEP-0060 example (there is no real formal description). When changing the node configuration, the max_items can't be set to a number lower than the total number of items in the node (the configuration will then be rejected), this is to avoid accidental deletion of items. diff -r f124ed5ea78b -r c21f31355ab9 CHANGELOG --- a/CHANGELOG Sat Feb 22 13:06:51 2020 +0100 +++ b/CHANGELOG Tue Mar 10 11:11:38 2020 +0100 @@ -1,6 +1,7 @@ v 0.4.0 (NOT RELEASED YET): - Python 3 port - publish-options implementation + - max_items configuration option - bug fixes v 0.3.0 (16/08/2019) diff -r f124ed5ea78b -r c21f31355ab9 sat_pubsub/backend.py --- a/sat_pubsub/backend.py Sat Feb 22 13:06:51 2020 +0100 +++ b/sat_pubsub/backend.py Tue Mar 10 11:11:38 2020 +0100 @@ -138,6 +138,9 @@ const.OPT_PERSIST_ITEMS: {"type": "boolean", "label": "Persist items to storage"}, + const.OPT_MAX_ITEMS: + {"type": "text-single", + "label": 'number of items to keep ("max" for unlimited)'}, const.OPT_DELIVER_PAYLOADS: {"type": "boolean", "label": "Deliver payloads with event notifications"}, @@ -670,16 +673,44 @@ d = self.storage.getNode(nodeIdentifier, pep, recipient) d.addCallback(_getAffiliation, requestor) - d.addCallback(self._doSetNodeConfiguration, requestor, options) + d.addCallback( + lambda result: defer.ensureDeferred( + self._doSetNodeConfiguration(result, requestor, options)) + ) return d - def _doSetNodeConfiguration(self, result, requestor, options): + async def _doSetNodeConfiguration(self, result, requestor, options): node, affiliation = result if affiliation != 'owner' and not self.isAdmin(requestor): raise error.Forbidden() - return node.setConfiguration(options) + max_items = options.get(const.OPT_MAX_ITEMS, '').strip().lower() + if not max_items: + pass + elif max_items == 'max': + # FIXME: we use "0" for max for now, but as "0" may be used as legal value + # in XEP-0060 ("max" is used otherwise), we should use "None" instead. + # This require a schema update, as NULL is not allowed at the moment + options.fields[const.OPT_MAX_ITEMS].value = "0" + else: + # max_items is a number, we have to check that it's not bigger than the + # total number of items on this node + try: + max_items = int(max_items) + except ValueError: + raise error.InvalidConfigurationValue('Invalid "max_items" value') + else: + options.fields[const.OPT_MAX_ITEMS].value = str(max_items) + if max_items: + items_count = await node.getItemsCount(None, True) + if max_items < items_count: + raise error.ConstraintFailed( + "\"max_items\" can't be set to a value lower than the total " + "number of items on this node. Please increase \"max_items\" " + "or delete items from this node") + + return await node.setConfiguration(options) def getNodeSchema(self, nodeIdentifier, pep, recipient): if not nodeIdentifier: diff -r f124ed5ea78b -r c21f31355ab9 sat_pubsub/const.py --- a/sat_pubsub/const.py Sat Feb 22 13:06:51 2020 +0100 +++ b/sat_pubsub/const.py Tue Mar 10 11:11:38 2020 +0100 @@ -62,6 +62,7 @@ OPT_ACCESS_MODEL = 'pubsub#access_model' OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed' OPT_PERSIST_ITEMS = "pubsub#persist_items" +OPT_MAX_ITEMS = "pubsub#max_items" OPT_DELIVER_PAYLOADS = "pubsub#deliver_payloads" OPT_SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item" OPT_PUBLISH_MODEL = 'pubsub#publish_model' diff -r f124ed5ea78b -r c21f31355ab9 sat_pubsub/pgsql_storage.py --- a/sat_pubsub/pgsql_storage.py Sat Feb 22 13:06:51 2020 +0100 +++ b/sat_pubsub/pgsql_storage.py Tue Mar 10 11:11:38 2020 +0100 @@ -109,6 +109,7 @@ defaultConfig = { 'leaf': { const.OPT_PERSIST_ITEMS: True, + const.OPT_MAX_ITEMS: 'max', const.OPT_DELIVER_PAYLOADS: True, const.OPT_SEND_LAST_PUBLISHED_ITEM: 'on_sub', const.OPT_ACCESS_MODEL: const.VAL_AMODEL_DEFAULT, @@ -142,22 +143,21 @@ def _buildNode(self, row): """Build a note class from database result row""" - configuration = {} - if not row: raise error.NodeNotFound() if row[2] == 'leaf': configuration = { - 'pubsub#persist_items': row[3], - 'pubsub#deliver_payloads': row[4], - 'pubsub#send_last_published_item': row[5], - const.OPT_ACCESS_MODEL:row[6], - const.OPT_PUBLISH_MODEL:row[7], - const.OPT_SERIAL_IDS:row[8], - const.OPT_CONSISTENT_PUBLISHER:row[9], + const.OPT_PERSIST_ITEMS: row[3], + const.OPT_MAX_ITEMS: 'max' if row[4] == 0 else str(row[4]), + const.OPT_DELIVER_PAYLOADS: row[5], + const.OPT_SEND_LAST_PUBLISHED_ITEM: row[6], + const.OPT_ACCESS_MODEL:row[7], + const.OPT_PUBLISH_MODEL:row[8], + const.OPT_SERIAL_IDS:row[9], + const.OPT_CONSISTENT_PUBLISHER:row[10], } - schema = row[10] + schema = row[11] if schema is not None: schema = parseXml(schema) node = LeafNode(row[0], row[1], configuration, schema) @@ -165,10 +165,10 @@ return node elif row[2] == 'collection': configuration = { - 'pubsub#deliver_payloads': row[4], - 'pubsub#send_last_published_item': row[5], - const.OPT_ACCESS_MODEL: row[6], - const.OPT_PUBLISH_MODEL:row[7], + const.OPT_DELIVER_PAYLOADS: row[5], + const.OPT_SEND_LAST_PUBLISHED_ITEM: row[6], + const.OPT_ACCESS_MODEL: row[7], + const.OPT_PUBLISH_MODEL:row[8], } node = CollectionNode(row[0], row[1], configuration, None) node.dbpool = self.dbpool @@ -188,6 +188,7 @@ node, node_type, persist_items, + max_items, deliver_payloads, send_last_published_item, access_model, @@ -210,6 +211,7 @@ node, node_type, persist_items, + max_items, deliver_payloads, send_last_published_item, access_model, @@ -537,6 +539,7 @@ self._checkNodeExists(cursor) self._configurationTriggers(cursor, self.nodeDbId, self._config, config) cursor.execute("""UPDATE nodes SET persist_items=%s, + max_items=%s, deliver_payloads=%s, send_last_published_item=%s, access_model=%s, @@ -545,6 +548,7 @@ consistent_publisher=%s WHERE node_id=%s""", (config[const.OPT_PERSIST_ITEMS], + config[const.OPT_MAX_ITEMS], config[const.OPT_DELIVER_PAYLOADS], config[const.OPT_SEND_LAST_PUBLISHED_ITEM], config[const.OPT_ACCESS_MODEL], @@ -951,7 +955,8 @@ if access_model == const.VAL_AMODEL_PUBLISHER_ROSTER: if const.OPT_ROSTER_GROUPS_ALLOWED in item_config: - item_config.fields[const.OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' #XXX: needed to force list if there is only one value + # XXX: needed to force list if there is only one value + item_config.fields[const.OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' allowed_groups = item_config[const.OPT_ROSTER_GROUPS_ALLOWED] else: allowed_groups = [] @@ -961,6 +966,24 @@ VALUES (%s,%s)""" , (item_id, group)) # TODO: whitelist access model + max_items = self._config.get(const.OPT_MAX_ITEMS, 'max') + + if max_items != 'max': + try: + max_items = int(self._config[const.OPT_MAX_ITEMS]) + except ValueError: + log.err(f"Invalid max_items value: {max_items!r}") + else: + if max_items > 0: + # we delete all items above the requested max + cursor.execute( + "DELETE FROM items WHERE node_id=%s and item_id in (SELECT " + "item_id FROM items WHERE node_id=%s ORDER BY items.updated " + "DESC, items.item_id DESC OFFSET %s)" , + (self.nodeDbId, self.nodeDbId, max_items) + ) + + def _storeCategories(self, cursor, item_id, categories, update=False): # TODO: handle canonical form if update: