We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Crash on DELETE /buckets when quota plugin is enabled
Another collateral damage of #945 

A `http DELETE /v1/buckets` when the quota plugin is enabled leads to:

```
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/zope/interface/adapter.py", line 598, in subscribers
    subscription(*objects)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py", line 130, in subscriber_wrapper
    return derived_subscriber(*arg)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py", line 103, in derived_subscriber
    return subscriber(arg[0])
  File "/home/mathieu/Code/Mozilla/kinto/kinto/plugins/quotas/listener.py", line 47, in on_resource_changed
    bucket_id = payload['bucket_id']
KeyError: 'bucket_id'

```
Crash on DELETE /buckets when quota plugin is enabled
Another collateral damage of #945 

A `http DELETE /v1/buckets` when the quota plugin is enabled leads to:

```
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/zope/interface/adapter.py", line 598, in subscribers
    subscription(*objects)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py", line 130, in subscriber_wrapper
    return derived_subscriber(*arg)
  File "/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py", line 103, in derived_subscriber
    return subscriber(arg[0])
  File "/home/mathieu/Code/Mozilla/kinto/kinto/plugins/quotas/listener.py", line 47, in on_resource_changed
    bucket_id = payload['bucket_id']
KeyError: 'bucket_id'

```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kinto/plugins/quotas/listener.py`
Content:
```
1 import copy
2 
3 from pyramid.httpexceptions import HTTPInsufficientStorage
4 from kinto.core.errors import http_error, ERRORS
5 from kinto.core.storage.exceptions import RecordNotFoundError
6 from kinto.core.utils import instance_uri
7 
8 from .utils import record_size
9 
10 
11 QUOTA_RESOURCE_NAME = 'quota'
12 BUCKET_QUOTA_OBJECT_ID = 'bucket_info'
13 COLLECTION_QUOTA_OBJECT_ID = 'collection_info'
14 
15 
16 def get_bucket_settings(settings, bucket_id, name):
17     return settings.get(
18         # Bucket specific
19         'quotas.bucket_{}_{}'.format(bucket_id, name),
20         # Global to all buckets
21         settings.get('quotas.bucket_{}'.format(name), None))
22 
23 
24 def get_collection_settings(settings, bucket_id, collection_id, name):
25     return settings.get(
26         # Specific for a given bucket collection
27         'quotas.collection_{}_{}_{}'.format(bucket_id, collection_id, name),
28         # Specific to given bucket collections
29         settings.get('quotas.collection_{}_{}'.format(bucket_id, name),
30                      # Global to all buckets collections
31                      settings.get('quotas.collection_{}'.format(name), None)))
32 
33 
34 def on_resource_changed(event):
35     """
36     Everytime an object is created/changed/deleted, we update the
37     bucket counters.
38 
39     If a new object exceeds the quotas, we reject the request.
40     """
41     payload = event.payload
42     action = payload['action']
43     resource_name = payload['resource_name']
44     event_uri = payload['uri']
45 
46     settings = event.request.registry.settings
47 
48     bucket_id = payload['bucket_id']
49     bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)
50     collection_id = None
51     collection_uri = None
52     if 'collection_id' in payload:
53         collection_id = payload['collection_id']
54         collection_uri = instance_uri(event.request,
55                                       'collection',
56                                       bucket_id=bucket_id,
57                                       id=collection_id)
58 
59     bucket_max_bytes = get_bucket_settings(settings, bucket_id, 'max_bytes')
60     bucket_max_items = get_bucket_settings(settings, bucket_id, 'max_items')
61     bucket_max_bytes_per_item = get_bucket_settings(settings, bucket_id,
62                                                     'max_bytes_per_item')
63     collection_max_bytes = get_collection_settings(settings, bucket_id,
64                                                    collection_id, 'max_bytes')
65     collection_max_items = get_collection_settings(settings, bucket_id,
66                                                    collection_id, 'max_items')
67     collection_max_bytes_per_item = get_collection_settings(
68         settings, bucket_id, collection_id, 'max_bytes_per_item')
69 
70     max_bytes_per_item = (collection_max_bytes_per_item or
71                           bucket_max_bytes_per_item)
72 
73     storage = event.request.registry.storage
74 
75     if action == 'delete' and resource_name == 'bucket':
76         # Deleting a bucket already deletes everything underneath (including
77         # quotas info). See kinto/views/bucket.
78         return
79 
80     targets = []
81     for impacted in event.impacted_records:
82         target = impacted['new' if action != 'delete' else 'old']
83         # On POST .../records, the URI does not contain the newly created
84         # record id.
85         obj_id = target['id']
86         parts = event_uri.split('/')
87         if resource_name in parts[-1]:
88             parts.append(obj_id)
89         else:
90             # Make sure the id is correct on grouped events.
91             parts[-1] = obj_id
92         uri = '/'.join(parts)
93 
94         old = impacted.get('old', {})
95         new = impacted.get('new', {})
96 
97         targets.append((uri, obj_id, old, new))
98 
99     try:
100         bucket_info = copy.deepcopy(
101             storage.get(parent_id=bucket_uri,
102                         collection_id=QUOTA_RESOURCE_NAME,
103                         object_id=BUCKET_QUOTA_OBJECT_ID))
104     except RecordNotFoundError:
105         bucket_info = {
106             "collection_count": 0,
107             "record_count": 0,
108             "storage_size": 0,
109         }
110 
111     collection_info = {
112         "record_count": 0,
113         "storage_size": 0,
114     }
115     if collection_id:
116         try:
117             collection_info = copy.deepcopy(
118                 storage.get(parent_id=collection_uri,
119                             collection_id=QUOTA_RESOURCE_NAME,
120                             object_id=COLLECTION_QUOTA_OBJECT_ID))
121         except RecordNotFoundError:
122             pass
123 
124     # Update the bucket quotas values for each impacted record.
125     for (uri, obj_id, old, new) in targets:
126         old_size = record_size(old)
127         new_size = record_size(new)
128 
129         if max_bytes_per_item is not None and action != "delete":
130             if new_size > max_bytes_per_item:
131                 message = ("Maximum bytes per object exceeded "
132                            "({} > {} Bytes.".format(new_size, max_bytes_per_item))
133                 raise http_error(HTTPInsufficientStorage(),
134                                  errno=ERRORS.FORBIDDEN.value,
135                                  message=message)
136 
137         if action == 'create':
138             bucket_info['storage_size'] += new_size
139             if resource_name == 'collection':
140                 bucket_info['collection_count'] += 1
141                 collection_info['storage_size'] += new_size
142             if resource_name == 'record':
143                 bucket_info['record_count'] += 1
144                 collection_info['record_count'] += 1
145                 collection_info['storage_size'] += new_size
146         elif action == 'update':
147             bucket_info['storage_size'] -= old_size
148             bucket_info['storage_size'] += new_size
149             if resource_name in ('collection', 'record'):
150                 collection_info['storage_size'] -= old_size
151                 collection_info['storage_size'] += new_size
152         else:   # action == 'delete':
153             bucket_info['storage_size'] -= old_size
154             if resource_name == 'collection':
155                 collection_uri = uri
156                 bucket_info['collection_count'] -= 1
157                 # When we delete the collection all the records in it
158                 # are deleted without notification.
159                 collection_records, _ = storage.get_all(
160                     collection_id='record',
161                     parent_id=collection_uri)
162                 for r in collection_records:
163                     old_record_size = record_size(r)
164                     bucket_info['record_count'] -= 1
165                     bucket_info['storage_size'] -= old_record_size
166                     collection_info['record_count'] -= 1
167                     collection_info['storage_size'] -= old_record_size
168                 collection_info['storage_size'] -= old_size
169 
170             if resource_name == 'record':
171                 bucket_info['record_count'] -= 1
172                 collection_info['record_count'] -= 1
173                 collection_info['storage_size'] -= old_size
174 
175     if bucket_max_bytes is not None:
176         if bucket_info['storage_size'] > bucket_max_bytes:
177             message = ("Bucket maximum total size exceeded "
178                        "({} > {} Bytes). ".format(bucket_info['storage_size'],
179                                                   bucket_max_bytes))
180             raise http_error(HTTPInsufficientStorage(),
181                              errno=ERRORS.FORBIDDEN.value,
182                              message=message)
183 
184     if bucket_max_items is not None:
185         if bucket_info['record_count'] > bucket_max_items:
186             message = ("Bucket maximum number of objects exceeded "
187                        "({} > {} objects).".format(bucket_info['record_count'],
188                                                    bucket_max_items))
189             raise http_error(HTTPInsufficientStorage(),
190                              errno=ERRORS.FORBIDDEN.value,
191                              message=message)
192 
193     if collection_max_bytes is not None:
194         if collection_info['storage_size'] > collection_max_bytes:
195             message = ("Collection maximum size exceeded "
196                        "({} > {} Bytes).".format(collection_info['storage_size'],
197                                                  collection_max_bytes))
198             raise http_error(HTTPInsufficientStorage(),
199                              errno=ERRORS.FORBIDDEN.value,
200                              message=message)
201 
202     if collection_max_items is not None:
203         if collection_info['record_count'] > collection_max_items:
204             message = ("Collection maximum number of objects exceeded "
205                        "({} > {} objects).".format(collection_info['record_count'],
206                                                    collection_max_items))
207             raise http_error(HTTPInsufficientStorage(),
208                              errno=ERRORS.FORBIDDEN.value,
209                              message=message)
210 
211     storage.update(parent_id=bucket_uri,
212                    collection_id=QUOTA_RESOURCE_NAME,
213                    object_id=BUCKET_QUOTA_OBJECT_ID,
214                    record=bucket_info)
215 
216     if collection_id:
217         if action == 'delete' and resource_name == 'collection':
218             # Deleting a collection already deletes everything underneath
219             # (including quotas info). See kinto/views/collection.
220             return
221         else:
222             storage.update(parent_id=collection_uri,
223                            collection_id=QUOTA_RESOURCE_NAME,
224                            object_id=COLLECTION_QUOTA_OBJECT_ID,
225                            record=collection_info)
226
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kinto/plugins/quotas/listener.py b/kinto/plugins/quotas/listener.py
--- a/kinto/plugins/quotas/listener.py
+++ b/kinto/plugins/quotas/listener.py
@@ -41,10 +41,15 @@
     payload = event.payload
     action = payload['action']
     resource_name = payload['resource_name']
-    event_uri = payload['uri']
+
+    if action == 'delete' and resource_name == 'bucket':
+        # Deleting a bucket already deletes everything underneath (including
+        # quotas info). See kinto/views/bucket.
+        return
 
     settings = event.request.registry.settings
 
+    event_uri = payload['uri']
     bucket_id = payload['bucket_id']
     bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)
     collection_id = None
@@ -72,11 +77,6 @@
 
     storage = event.request.registry.storage
 
-    if action == 'delete' and resource_name == 'bucket':
-        # Deleting a bucket already deletes everything underneath (including
-        # quotas info). See kinto/views/bucket.
-        return
-
     targets = []
     for impacted in event.impacted_records:
         target = impacted['new' if action != 'delete' else 'old']

{"golden_diff": "diff --git a/kinto/plugins/quotas/listener.py b/kinto/plugins/quotas/listener.py\n--- a/kinto/plugins/quotas/listener.py\n+++ b/kinto/plugins/quotas/listener.py\n@@ -41,10 +41,15 @@\n     payload = event.payload\n     action = payload['action']\n     resource_name = payload['resource_name']\n-    event_uri = payload['uri']\n+\n+    if action == 'delete' and resource_name == 'bucket':\n+        # Deleting a bucket already deletes everything underneath (including\n+        # quotas info). See kinto/views/bucket.\n+        return\n \n     settings = event.request.registry.settings\n \n+    event_uri = payload['uri']\n     bucket_id = payload['bucket_id']\n     bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)\n     collection_id = None\n@@ -72,11 +77,6 @@\n \n     storage = event.request.registry.storage\n \n-    if action == 'delete' and resource_name == 'bucket':\n-        # Deleting a bucket already deletes everything underneath (including\n-        # quotas info). See kinto/views/bucket.\n-        return\n-\n     targets = []\n     for impacted in event.impacted_records:\n         target = impacted['new' if action != 'delete' else 'old']\n", "issue": "Crash on DELETE /buckets when quota plugin is enabled\nAnother collateral damage of #945 \r\n\r\nA `http DELETE /v1/buckets` when the quota plugin is enabled leads to:\r\n\r\n```\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/zope/interface/adapter.py\", line 598, in subscribers\r\n    subscription(*objects)\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py\", line 130, in subscriber_wrapper\r\n    return derived_subscriber(*arg)\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py\", line 103, in derived_subscriber\r\n    return subscriber(arg[0])\r\n  File \"/home/mathieu/Code/Mozilla/kinto/kinto/plugins/quotas/listener.py\", line 47, in on_resource_changed\r\n    bucket_id = payload['bucket_id']\r\nKeyError: 'bucket_id'\r\n\r\n```\nCrash on DELETE /buckets when quota plugin is enabled\nAnother collateral damage of #945 \r\n\r\nA `http DELETE /v1/buckets` when the quota plugin is enabled leads to:\r\n\r\n```\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/zope/interface/adapter.py\", line 598, in subscribers\r\n    subscription(*objects)\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py\", line 130, in subscriber_wrapper\r\n    return derived_subscriber(*arg)\r\n  File \"/home/mathieu/Code/Mozilla/kinto/.venv/lib/python3.5/site-packages/pyramid/config/adapters.py\", line 103, in derived_subscriber\r\n    return subscriber(arg[0])\r\n  File \"/home/mathieu/Code/Mozilla/kinto/kinto/plugins/quotas/listener.py\", line 47, in on_resource_changed\r\n    bucket_id = payload['bucket_id']\r\nKeyError: 'bucket_id'\r\n\r\n```\n", "before_files": [{"content": "import copy\n\nfrom pyramid.httpexceptions import HTTPInsufficientStorage\nfrom kinto.core.errors import http_error, ERRORS\nfrom kinto.core.storage.exceptions import RecordNotFoundError\nfrom kinto.core.utils import instance_uri\n\nfrom .utils import record_size\n\n\nQUOTA_RESOURCE_NAME = 'quota'\nBUCKET_QUOTA_OBJECT_ID = 'bucket_info'\nCOLLECTION_QUOTA_OBJECT_ID = 'collection_info'\n\n\ndef get_bucket_settings(settings, bucket_id, name):\n    return settings.get(\n        # Bucket specific\n        'quotas.bucket_{}_{}'.format(bucket_id, name),\n        # Global to all buckets\n        settings.get('quotas.bucket_{}'.format(name), None))\n\n\ndef get_collection_settings(settings, bucket_id, collection_id, name):\n    return settings.get(\n        # Specific for a given bucket collection\n        'quotas.collection_{}_{}_{}'.format(bucket_id, collection_id, name),\n        # Specific to given bucket collections\n        settings.get('quotas.collection_{}_{}'.format(bucket_id, name),\n                     # Global to all buckets collections\n                     settings.get('quotas.collection_{}'.format(name), None)))\n\n\ndef on_resource_changed(event):\n    \"\"\"\n    Everytime an object is created/changed/deleted, we update the\n    bucket counters.\n\n    If a new object exceeds the quotas, we reject the request.\n    \"\"\"\n    payload = event.payload\n    action = payload['action']\n    resource_name = payload['resource_name']\n    event_uri = payload['uri']\n\n    settings = event.request.registry.settings\n\n    bucket_id = payload['bucket_id']\n    bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)\n    collection_id = None\n    collection_uri = None\n    if 'collection_id' in payload:\n        collection_id = payload['collection_id']\n        collection_uri = instance_uri(event.request,\n                                      'collection',\n                                      bucket_id=bucket_id,\n                                      id=collection_id)\n\n    bucket_max_bytes = get_bucket_settings(settings, bucket_id, 'max_bytes')\n    bucket_max_items = get_bucket_settings(settings, bucket_id, 'max_items')\n    bucket_max_bytes_per_item = get_bucket_settings(settings, bucket_id,\n                                                    'max_bytes_per_item')\n    collection_max_bytes = get_collection_settings(settings, bucket_id,\n                                                   collection_id, 'max_bytes')\n    collection_max_items = get_collection_settings(settings, bucket_id,\n                                                   collection_id, 'max_items')\n    collection_max_bytes_per_item = get_collection_settings(\n        settings, bucket_id, collection_id, 'max_bytes_per_item')\n\n    max_bytes_per_item = (collection_max_bytes_per_item or\n                          bucket_max_bytes_per_item)\n\n    storage = event.request.registry.storage\n\n    if action == 'delete' and resource_name == 'bucket':\n        # Deleting a bucket already deletes everything underneath (including\n        # quotas info). See kinto/views/bucket.\n        return\n\n    targets = []\n    for impacted in event.impacted_records:\n        target = impacted['new' if action != 'delete' else 'old']\n        # On POST .../records, the URI does not contain the newly created\n        # record id.\n        obj_id = target['id']\n        parts = event_uri.split('/')\n        if resource_name in parts[-1]:\n            parts.append(obj_id)\n        else:\n            # Make sure the id is correct on grouped events.\n            parts[-1] = obj_id\n        uri = '/'.join(parts)\n\n        old = impacted.get('old', {})\n        new = impacted.get('new', {})\n\n        targets.append((uri, obj_id, old, new))\n\n    try:\n        bucket_info = copy.deepcopy(\n            storage.get(parent_id=bucket_uri,\n                        collection_id=QUOTA_RESOURCE_NAME,\n                        object_id=BUCKET_QUOTA_OBJECT_ID))\n    except RecordNotFoundError:\n        bucket_info = {\n            \"collection_count\": 0,\n            \"record_count\": 0,\n            \"storage_size\": 0,\n        }\n\n    collection_info = {\n        \"record_count\": 0,\n        \"storage_size\": 0,\n    }\n    if collection_id:\n        try:\n            collection_info = copy.deepcopy(\n                storage.get(parent_id=collection_uri,\n                            collection_id=QUOTA_RESOURCE_NAME,\n                            object_id=COLLECTION_QUOTA_OBJECT_ID))\n        except RecordNotFoundError:\n            pass\n\n    # Update the bucket quotas values for each impacted record.\n    for (uri, obj_id, old, new) in targets:\n        old_size = record_size(old)\n        new_size = record_size(new)\n\n        if max_bytes_per_item is not None and action != \"delete\":\n            if new_size > max_bytes_per_item:\n                message = (\"Maximum bytes per object exceeded \"\n                           \"({} > {} Bytes.\".format(new_size, max_bytes_per_item))\n                raise http_error(HTTPInsufficientStorage(),\n                                 errno=ERRORS.FORBIDDEN.value,\n                                 message=message)\n\n        if action == 'create':\n            bucket_info['storage_size'] += new_size\n            if resource_name == 'collection':\n                bucket_info['collection_count'] += 1\n                collection_info['storage_size'] += new_size\n            if resource_name == 'record':\n                bucket_info['record_count'] += 1\n                collection_info['record_count'] += 1\n                collection_info['storage_size'] += new_size\n        elif action == 'update':\n            bucket_info['storage_size'] -= old_size\n            bucket_info['storage_size'] += new_size\n            if resource_name in ('collection', 'record'):\n                collection_info['storage_size'] -= old_size\n                collection_info['storage_size'] += new_size\n        else:   # action == 'delete':\n            bucket_info['storage_size'] -= old_size\n            if resource_name == 'collection':\n                collection_uri = uri\n                bucket_info['collection_count'] -= 1\n                # When we delete the collection all the records in it\n                # are deleted without notification.\n                collection_records, _ = storage.get_all(\n                    collection_id='record',\n                    parent_id=collection_uri)\n                for r in collection_records:\n                    old_record_size = record_size(r)\n                    bucket_info['record_count'] -= 1\n                    bucket_info['storage_size'] -= old_record_size\n                    collection_info['record_count'] -= 1\n                    collection_info['storage_size'] -= old_record_size\n                collection_info['storage_size'] -= old_size\n\n            if resource_name == 'record':\n                bucket_info['record_count'] -= 1\n                collection_info['record_count'] -= 1\n                collection_info['storage_size'] -= old_size\n\n    if bucket_max_bytes is not None:\n        if bucket_info['storage_size'] > bucket_max_bytes:\n            message = (\"Bucket maximum total size exceeded \"\n                       \"({} > {} Bytes). \".format(bucket_info['storage_size'],\n                                                  bucket_max_bytes))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if bucket_max_items is not None:\n        if bucket_info['record_count'] > bucket_max_items:\n            message = (\"Bucket maximum number of objects exceeded \"\n                       \"({} > {} objects).\".format(bucket_info['record_count'],\n                                                   bucket_max_items))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if collection_max_bytes is not None:\n        if collection_info['storage_size'] > collection_max_bytes:\n            message = (\"Collection maximum size exceeded \"\n                       \"({} > {} Bytes).\".format(collection_info['storage_size'],\n                                                 collection_max_bytes))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if collection_max_items is not None:\n        if collection_info['record_count'] > collection_max_items:\n            message = (\"Collection maximum number of objects exceeded \"\n                       \"({} > {} objects).\".format(collection_info['record_count'],\n                                                   collection_max_items))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    storage.update(parent_id=bucket_uri,\n                   collection_id=QUOTA_RESOURCE_NAME,\n                   object_id=BUCKET_QUOTA_OBJECT_ID,\n                   record=bucket_info)\n\n    if collection_id:\n        if action == 'delete' and resource_name == 'collection':\n            # Deleting a collection already deletes everything underneath\n            # (including quotas info). See kinto/views/collection.\n            return\n        else:\n            storage.update(parent_id=collection_uri,\n                           collection_id=QUOTA_RESOURCE_NAME,\n                           object_id=COLLECTION_QUOTA_OBJECT_ID,\n                           record=collection_info)\n", "path": "kinto/plugins/quotas/listener.py"}], "after_files": [{"content": "import copy\n\nfrom pyramid.httpexceptions import HTTPInsufficientStorage\nfrom kinto.core.errors import http_error, ERRORS\nfrom kinto.core.storage.exceptions import RecordNotFoundError\nfrom kinto.core.utils import instance_uri\n\nfrom .utils import record_size\n\n\nQUOTA_RESOURCE_NAME = 'quota'\nBUCKET_QUOTA_OBJECT_ID = 'bucket_info'\nCOLLECTION_QUOTA_OBJECT_ID = 'collection_info'\n\n\ndef get_bucket_settings(settings, bucket_id, name):\n    return settings.get(\n        # Bucket specific\n        'quotas.bucket_{}_{}'.format(bucket_id, name),\n        # Global to all buckets\n        settings.get('quotas.bucket_{}'.format(name), None))\n\n\ndef get_collection_settings(settings, bucket_id, collection_id, name):\n    return settings.get(\n        # Specific for a given bucket collection\n        'quotas.collection_{}_{}_{}'.format(bucket_id, collection_id, name),\n        # Specific to given bucket collections\n        settings.get('quotas.collection_{}_{}'.format(bucket_id, name),\n                     # Global to all buckets collections\n                     settings.get('quotas.collection_{}'.format(name), None)))\n\n\ndef on_resource_changed(event):\n    \"\"\"\n    Everytime an object is created/changed/deleted, we update the\n    bucket counters.\n\n    If a new object exceeds the quotas, we reject the request.\n    \"\"\"\n    payload = event.payload\n    action = payload['action']\n    resource_name = payload['resource_name']\n\n    if action == 'delete' and resource_name == 'bucket':\n        # Deleting a bucket already deletes everything underneath (including\n        # quotas info). See kinto/views/bucket.\n        return\n\n    settings = event.request.registry.settings\n\n    event_uri = payload['uri']\n    bucket_id = payload['bucket_id']\n    bucket_uri = instance_uri(event.request, 'bucket', id=bucket_id)\n    collection_id = None\n    collection_uri = None\n    if 'collection_id' in payload:\n        collection_id = payload['collection_id']\n        collection_uri = instance_uri(event.request,\n                                      'collection',\n                                      bucket_id=bucket_id,\n                                      id=collection_id)\n\n    bucket_max_bytes = get_bucket_settings(settings, bucket_id, 'max_bytes')\n    bucket_max_items = get_bucket_settings(settings, bucket_id, 'max_items')\n    bucket_max_bytes_per_item = get_bucket_settings(settings, bucket_id,\n                                                    'max_bytes_per_item')\n    collection_max_bytes = get_collection_settings(settings, bucket_id,\n                                                   collection_id, 'max_bytes')\n    collection_max_items = get_collection_settings(settings, bucket_id,\n                                                   collection_id, 'max_items')\n    collection_max_bytes_per_item = get_collection_settings(\n        settings, bucket_id, collection_id, 'max_bytes_per_item')\n\n    max_bytes_per_item = (collection_max_bytes_per_item or\n                          bucket_max_bytes_per_item)\n\n    storage = event.request.registry.storage\n\n    targets = []\n    for impacted in event.impacted_records:\n        target = impacted['new' if action != 'delete' else 'old']\n        # On POST .../records, the URI does not contain the newly created\n        # record id.\n        obj_id = target['id']\n        parts = event_uri.split('/')\n        if resource_name in parts[-1]:\n            parts.append(obj_id)\n        else:\n            # Make sure the id is correct on grouped events.\n            parts[-1] = obj_id\n        uri = '/'.join(parts)\n\n        old = impacted.get('old', {})\n        new = impacted.get('new', {})\n\n        targets.append((uri, obj_id, old, new))\n\n    try:\n        bucket_info = copy.deepcopy(\n            storage.get(parent_id=bucket_uri,\n                        collection_id=QUOTA_RESOURCE_NAME,\n                        object_id=BUCKET_QUOTA_OBJECT_ID))\n    except RecordNotFoundError:\n        bucket_info = {\n            \"collection_count\": 0,\n            \"record_count\": 0,\n            \"storage_size\": 0,\n        }\n\n    collection_info = {\n        \"record_count\": 0,\n        \"storage_size\": 0,\n    }\n    if collection_id:\n        try:\n            collection_info = copy.deepcopy(\n                storage.get(parent_id=collection_uri,\n                            collection_id=QUOTA_RESOURCE_NAME,\n                            object_id=COLLECTION_QUOTA_OBJECT_ID))\n        except RecordNotFoundError:\n            pass\n\n    # Update the bucket quotas values for each impacted record.\n    for (uri, obj_id, old, new) in targets:\n        old_size = record_size(old)\n        new_size = record_size(new)\n\n        if max_bytes_per_item is not None and action != \"delete\":\n            if new_size > max_bytes_per_item:\n                message = (\"Maximum bytes per object exceeded \"\n                           \"({} > {} Bytes.\".format(new_size, max_bytes_per_item))\n                raise http_error(HTTPInsufficientStorage(),\n                                 errno=ERRORS.FORBIDDEN.value,\n                                 message=message)\n\n        if action == 'create':\n            bucket_info['storage_size'] += new_size\n            if resource_name == 'collection':\n                bucket_info['collection_count'] += 1\n                collection_info['storage_size'] += new_size\n            if resource_name == 'record':\n                bucket_info['record_count'] += 1\n                collection_info['record_count'] += 1\n                collection_info['storage_size'] += new_size\n        elif action == 'update':\n            bucket_info['storage_size'] -= old_size\n            bucket_info['storage_size'] += new_size\n            if resource_name in ('collection', 'record'):\n                collection_info['storage_size'] -= old_size\n                collection_info['storage_size'] += new_size\n        else:   # action == 'delete':\n            bucket_info['storage_size'] -= old_size\n            if resource_name == 'collection':\n                collection_uri = uri\n                bucket_info['collection_count'] -= 1\n                # When we delete the collection all the records in it\n                # are deleted without notification.\n                collection_records, _ = storage.get_all(\n                    collection_id='record',\n                    parent_id=collection_uri)\n                for r in collection_records:\n                    old_record_size = record_size(r)\n                    bucket_info['record_count'] -= 1\n                    bucket_info['storage_size'] -= old_record_size\n                    collection_info['record_count'] -= 1\n                    collection_info['storage_size'] -= old_record_size\n                collection_info['storage_size'] -= old_size\n\n            if resource_name == 'record':\n                bucket_info['record_count'] -= 1\n                collection_info['record_count'] -= 1\n                collection_info['storage_size'] -= old_size\n\n    if bucket_max_bytes is not None:\n        if bucket_info['storage_size'] > bucket_max_bytes:\n            message = (\"Bucket maximum total size exceeded \"\n                       \"({} > {} Bytes). \".format(bucket_info['storage_size'],\n                                                  bucket_max_bytes))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if bucket_max_items is not None:\n        if bucket_info['record_count'] > bucket_max_items:\n            message = (\"Bucket maximum number of objects exceeded \"\n                       \"({} > {} objects).\".format(bucket_info['record_count'],\n                                                   bucket_max_items))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if collection_max_bytes is not None:\n        if collection_info['storage_size'] > collection_max_bytes:\n            message = (\"Collection maximum size exceeded \"\n                       \"({} > {} Bytes).\".format(collection_info['storage_size'],\n                                                 collection_max_bytes))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    if collection_max_items is not None:\n        if collection_info['record_count'] > collection_max_items:\n            message = (\"Collection maximum number of objects exceeded \"\n                       \"({} > {} objects).\".format(collection_info['record_count'],\n                                                   collection_max_items))\n            raise http_error(HTTPInsufficientStorage(),\n                             errno=ERRORS.FORBIDDEN.value,\n                             message=message)\n\n    storage.update(parent_id=bucket_uri,\n                   collection_id=QUOTA_RESOURCE_NAME,\n                   object_id=BUCKET_QUOTA_OBJECT_ID,\n                   record=bucket_info)\n\n    if collection_id:\n        if action == 'delete' and resource_name == 'collection':\n            # Deleting a collection already deletes everything underneath\n            # (including quotas info). See kinto/views/collection.\n            return\n        else:\n            storage.update(parent_id=collection_uri,\n                           collection_id=QUOTA_RESOURCE_NAME,\n                           object_id=COLLECTION_QUOTA_OBJECT_ID,\n                           record=collection_info)\n", "path": "kinto/plugins/quotas/listener.py"}]}