From 47d153887fcec3ff51699dce46b12e51b946a787 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 22 Sep 2023 15:30:23 +0300 Subject: [PATCH 1/7] added test redis cache for `apps.json` endpoint Signed-off-by: Alexander Piskun --- .../api/v1/response_redis_cache.py | 47 +++++ nextcloudappstore/api/v1/urls.py | 3 +- nextcloudappstore/settings/base.py | 7 + poetry.lock | 162 +++++++++++++++--- pyproject.toml | 1 + 5 files changed, 197 insertions(+), 23 deletions(-) create mode 100644 nextcloudappstore/api/v1/response_redis_cache.py diff --git a/nextcloudappstore/api/v1/response_redis_cache.py b/nextcloudappstore/api/v1/response_redis_cache.py new file mode 100644 index 00000000000..f568e245b59 --- /dev/null +++ b/nextcloudappstore/api/v1/response_redis_cache.py @@ -0,0 +1,47 @@ +from functools import wraps + +from django.core.cache import caches +from django.http.response import HttpResponse +from django.utils.cache import get_conditional_response +from django.utils.http import quote_etag + + +def etag_redis(etag_func): + return condition_redis(etag_func) + + +def condition_redis(etag_func): + """Cache of reply for HTTP request in Redis, where Redis Key is the ETag of reply. + + .. note:: It is the dev version, and can be used only for the one endpoint + if all goes well it will be modified to support multiply endpoints. + """ + + def decorator(func): + @wraps(func) + def inner(request, *args, **kwargs): + # The value from etag_func() could be quoted or unquoted. + res_etag = etag_func(request, *args, **kwargs) if etag_func else None + res_etag = quote_etag(res_etag) if res_etag is not None else None + + response = get_conditional_response( + request, + etag=res_etag, + ) + if response is None: + cache = caches["default"] + cached_response = cache.get(res_etag) + if not cached_response: + response = func(request, *args, **kwargs) + if response.status_code == 200: + cache.set(res_etag, response.rendered_content, 10 * 60) + else: + response = HttpResponse(content=cached_response, content_type="application/json") + if request.method in ("GET", "HEAD"): + if res_etag: + response.headers.setdefault("ETag", res_etag) + return response + + return inner + + return decorator diff --git a/nextcloudappstore/api/v1/urls.py b/nextcloudappstore/api/v1/urls.py index 3d645140dc0..60b9ab6a98d 100644 --- a/nextcloudappstore/api/v1/urls.py +++ b/nextcloudappstore/api/v1/urls.py @@ -1,6 +1,7 @@ from django.urls import re_path from django.views.decorators.http import etag +from nextcloudappstore.api.v1.response_redis_cache import condition_redis from nextcloudappstore.api.v1.views import ( AppRatingView, AppRegisterView, @@ -26,7 +27,7 @@ urlpatterns = [ re_path(r"^platforms\.json$", etag(nextcloud_release_etag)(NextcloudReleaseView.as_view()), name="platforms"), re_path(r"^platform/(?P\d+\.\d+\.\d+)/apps\.json$", etag(apps_etag)(AppView.as_view()), name="app"), - re_path(r"^apps\.json$", etag(apps_all_etag)(AppsView.as_view()), name="apps"), + re_path(r"^apps\.json$", condition_redis(apps_all_etag)(AppsView.as_view()), name="apps"), re_path(r"^apps/releases/?$", AppReleaseView.as_view(), name="app-release-create"), re_path(r"^apps/?$", AppRegisterView.as_view(), name="app-register"), re_path(r"^apps/(?P[a-z0-9_]+)/?$", AppView.as_view(), name="app-delete"), diff --git a/nextcloudappstore/settings/base.py b/nextcloudappstore/settings/base.py index 6e9b034330b..cb8bc8f2274 100644 --- a/nextcloudappstore/settings/base.py +++ b/nextcloudappstore/settings/base.py @@ -357,3 +357,10 @@ # Account login bruteforce ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 10 ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 3600 # 1 hour in seconds + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/", + } +} diff --git a/poetry.lock b/poetry.lock index 97006c6b79a..5201fb24fb8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -28,6 +28,17 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "attrs" version = "23.1.0" @@ -601,6 +612,104 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "hiredis" +version = "2.2.3" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.7" +files = [ + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:9a1a80a8fa767f2fdc3870316a54b84fe9fc09fa6ab6a2686783de6a228a4604"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3f006c28c885deb99b670a5a66f367a175ab8955b0374029bad7111f5357dcd4"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffaf841546905d90ff189de7397aa56413b1ce5e54547f17a98f0ebf3a3b0a3b"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cadb0ac7ba3babfd804e425946bec9717b320564a1390f163a54af9365a720a"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33bc4721632ef9708fa44e5df0066053fccc8e65410a2c48573192517a533b48"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:227c5b4bcb60f89008c275d596e4a7b6625a6b3c827b8a66ae582eace7051f71"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61995eb826009d99ed8590747bc0da683a5f4fbb4faa8788166bf3810845cd5c"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f969edc851efe23010e0f53a64269f2629a9364135e9ec81c842e8b2277d0c1"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27e560eefb57914d742a837f1da98d3b29cb22eff013c8023b7cf52ae6e051d"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3759f4789ae1913b7df278dfc9e8749205b7a106f888cd2903d19461e24a7697"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6cb613148422c523945cdb8b6bed617856f2602fd8750e33773ede2616e55d5"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:1d274d5c511dfc03f83f997d3238eaa9b6ee3f982640979f509373cced891e98"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b7fe075e91b9d9cff40eba4fb6a8eff74964d3979a39be9a9ef58b1b4cb3604"}, + {file = "hiredis-2.2.3-cp310-cp310-win32.whl", hash = "sha256:77924b0d32fd1f493d3df15d9609ddf9d94c31a364022a6bf6b525ce9da75bea"}, + {file = "hiredis-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:dcb0569dd5bfe6004658cd0f229efa699a3169dcb4f77bd72e188adda302063d"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d115790f18daa99b5c11a506e48923b630ef712e9e4b40482af942c3d40638b8"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c3b8be557e08b234774925622e196f0ee36fe4eab66cd19df934d3efd8f3743"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f5446068197b35a11ccc697720c41879c8657e2e761aaa8311783aac84cef20"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa17a3b22b3726d54d7af20394f65d4a1735a842a4e0f557dc67a90f6965c4bc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7df645b6b7800e8b748c217fbd6a4ca8361bcb9a1ae6206cc02377833ec8a1aa"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fb9300959a0048138791f3d68359d61a788574ec9556bddf1fec07f2dbc5320"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d7e459fe7313925f395148d36d9b7f4f8dac65be06e45d7af356b187cef65fc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8eceffca3941775b646cd585cd19b275d382de43cc3327d22f7c75d7b003d481"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b17baf702c6e5b4bb66e1281a3efbb1d749c9d06cdb92b665ad81e03118f78fc"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e43e2b5acaad09cf48c032f7e4926392bb3a3f01854416cf6d82ebff94d5467"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a7205497d7276a81fe92951a29616ef96562ed2f91a02066f72b6f93cb34b40e"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:126623b03c31cb6ac3e0d138feb6fcc36dd43dd34fc7da7b7a0c38b5d75bc896"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:071c5814b850574036506a8118034f97c3cbf2fe9947ff45a27b07a48da56240"}, + {file = "hiredis-2.2.3-cp311-cp311-win32.whl", hash = "sha256:d1be9e30e675f5bc1cb534633324578f6f0944a1bcffe53242cf632f554f83b6"}, + {file = "hiredis-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9a7c987e161e3c58f992c63b7e26fea7fe0777f3b975799d23d65bbb8cb5899"}, + {file = "hiredis-2.2.3-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f2dcb8389fa3d453927b1299f46bdb38473c293c8269d5c777d33ea0e526b610"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2df98f5e071320c7d84e8bd07c0542acdd0a7519307fc31774d60e4b842ec4f"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a72e4a523cdfc521762137559c08dfa360a3caef63620be58c699d1717dac1"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9b9e5bde7030cae83aa900b5bd660decc65afd2db8c400f3c568c815a47ca2a"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2614f17e261f72efc2f19f5e5ff2ee19e2296570c0dcf33409e22be30710de"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46525fbd84523cac75af5bf524bc74aaac848beaf31b142d2df8a787d9b4bbc4"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1a4ce40ba11da9382c14da31f4f9e88c18f7d294f523decd0fadfb81f51ad18"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cda592405bbd29d53942e0389dc3fa77b49c362640210d7e94a10c14a677d4d"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5e6674a017629284ef373b50496d9fb1a89b85a20a7fa100ecd109484ec748e5"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:e62ec131816c6120eff40dffe43424e140264a15fa4ab88c301bd6a595913af3"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17e938d9d3ee92e1adbff361706f1c36cc60eeb3e3eeca7a3a353eae344f4c91"}, + {file = "hiredis-2.2.3-cp37-cp37m-win32.whl", hash = "sha256:95d2305fd2a7b179cacb48b10f618872fc565c175f9f62b854e8d1acac3e8a9e"}, + {file = "hiredis-2.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8f9dbe12f011a9b784f58faecc171d22465bb532c310bd588d769ba79a59ef5a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:5a4bcef114fc071d5f52c386c47f35aae0a5b43673197b9288a15b584da8fa3a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:232d0a70519865741ba56e1dfefd160a580ae78c30a1517bad47b3cf95a3bc7d"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9076ce8429785c85f824650735791738de7143f61f43ae9ed83e163c0ca0fa44"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec58fb7c2062f835595c12f0f02dcda76d0eb0831423cc191d1e18c9276648de"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2b34a6444b8f9c1e9f84bd2c639388e5d14f128afd14a869dfb3d9af893aa2"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:818dfd310aa1020a13cd08ee48e116dd8c3bb2e23b8161f8ac4df587dd5093d7"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d9ea6c8d4cbdeee2e0d43379ce2881e4af0454b00570677c59f33f2531cd38"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1eadbcd3de55ac42310ff82550d3302cb4efcd4e17d76646a17b6e7004bb42b"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:477c34c4489666dc73cb5e89dafe2617c3e13da1298917f73d55aac4696bd793"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:14824e457e4f5cda685c3345d125da13949bcf3bb1c88eb5d248c8d2c3dee08f"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9cd32326dfa6ce87edf754153b0105aca64486bebe93b9600ccff74fa0b224df"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:51341e70b467004dcbec3a6ce8c478d2d6241e0f6b01e4c56764afd5022e1e9d"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2443659c76b226267e2a04dbbb21bc2a3f91aa53bdc0c22964632753ae43a247"}, + {file = "hiredis-2.2.3-cp38-cp38-win32.whl", hash = "sha256:4e3e3e31423f888d396b1fc1f936936e52af868ac1ec17dd15e3eeba9dd4de24"}, + {file = "hiredis-2.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:20f509e3a1a20d6e5f5794fc37ceb21f70f409101fcfe7a8bde783894d51b369"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:d20891e3f33803b26d54c77fd5745878497091e33f4bbbdd454cf6e71aee8890"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:50171f985e17970f87d5a29e16603d1e5b03bdbf5c2691a37e6c912942a6b657"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9944a2cac25ffe049a7e89f306e11b900640837d1ef38d9be0eaa4a4e2b73a52"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a5c8019ff94988d56eb49b15de76fe83f6b42536d76edeb6565dbf7fe14b973"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a286ded34eb16501002e3713b3130c987366eee2ba0d58c33c72f27778e31676"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e974ad15eb32b1f537730dea70b93a4c3db7b026de3ad2b59da49c6f7454d"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08415ea74c1c29b9d6a4ca3dd0e810dc1af343c1d1d442e15ba133b11ab5be6a"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e17d04ea58ab8cf3f2dc52e875db16077c6357846006780086fff3189fb199d"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6ccdcb635dae85b006592f78e32d97f4bc7541cb27829d505f9c7fefcef48298"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69536b821dd1bc78058a6e7541743f8d82bf2d981b91280b14c4daa6cdc7faba"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3753df5f873d473f055e1f8837bfad0bd3b277c86f3c9bf058c58f14204cd901"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6f88cafe46612b6fa68e6dea49e25bebf160598bba00101caa51cc8c1f18d597"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33ee3ea5cad3a8cb339352cd230b411eb437a2e75d7736c4899acab32056ccdb"}, + {file = "hiredis-2.2.3-cp39-cp39-win32.whl", hash = "sha256:b4f3d06dc16671b88a13ae85d8ca92534c0b637d59e49f0558d040a691246422"}, + {file = "hiredis-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4f674e309cd055ee7a48304ceb8cf43265d859faf4d7d01d270ce45e976ae9d3"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8f280ab4e043b089777b43b4227bdc2035f88da5072ab36588e0ccf77d45d058"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c2a551f3b8a26f7940d6ee10b837810201754b8d7e6f6b1391655370882c5a"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c4e3c258eafaab21b174b17270a0cc093718d61cdbde8c03f85ec4bf835343"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc36a9dded458d4e37492fe3e619c6c83caae794d26ad925adbce61d592f8428"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4ed68a3b1ccb4313d2a42546fd7e7439ad4745918a48b6c9bcaa61e1e3e42634"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3bf4b5bae472630c229518e4a814b1b68f10a3d9b00aeaec45f1a330f03a0251"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33a94d264e6e12a79d9bb8af333b01dc286b9f39c99072ab5fef94ce1f018e17"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fa6811a618653164f918b891a0fa07052bd71a799defa5c44d167cac5557b26"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af33f370be90b48bbaf0dab32decbdcc522b1fa95d109020a963282086518a8e"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b9953d87418ac228f508d93898ab572775e4d3b0eeb886a1a7734553bcdaf291"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5e7bb4dd524f50b71c20ef5a12bd61da9b463f8894b18a06130942fe31509881"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89a258424158eb8b3ed9f65548d68998da334ef155d09488c5637723eb1cd697"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4a65276f6ecdebe75f2a53f578fbc40e8d2860658420d5e0611c56bbf5054c"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:334f2738700b20faa04a0d813366fb16ed17287430a6b50584161d5ad31ca6d7"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d194decd9608f11c777946f596f31d5aacad13972a0a87829ae1e6f2d26c1885"}, + {file = "hiredis-2.2.3.tar.gz", hash = "sha256:e75163773a309e56a9b58165cf5a50e0f84b755f6ff863b2c01a38918fe92daa"}, +] + [[package]] name = "idna" version = "3.4" @@ -822,16 +931,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1349,6 +1448,25 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "redis" +version = "5.0.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.0-py3-none-any.whl", hash = "sha256:06570d0b2d84d46c21defc550afbaada381af82f5b83e5b3777600e05d8e2ed0"}, + {file = "redis-5.0.0.tar.gz", hash = "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +hiredis = {version = ">=1.0.0", optional = true, markers = "extra == \"hiredis\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "requests" version = "2.31.0" @@ -1750,13 +1868,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.2" +version = "2.31.0.3" description = "Typing stubs for requests" optional = false python-versions = "*" files = [ - {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, - {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, + {file = "types-requests-2.31.0.3.tar.gz", hash = "sha256:d5d7a08965fca12bedf716eaf5430c6e3d0da9f3164a1dba2a7f3885f9ebe3c0"}, + {file = "types_requests-2.31.0.3-py3-none-any.whl", hash = "sha256:938f51653c757716aeca5d72c405c5e2befad8b0d330e3b385ce7f148e1b10dc"}, ] [package.dependencies] @@ -1797,13 +1915,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.4" +version = "2.0.5" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, + {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, ] [package.dependencies] @@ -1852,20 +1970,20 @@ h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.16.2" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "31428659140019915f2e80bea75d4893339e4489e7adce469b0a9b8cbfd231cb" +content-hash = "713457337e15a17defae0977d017270dad436d31708001ec41ff74e79249fe6b" diff --git a/pyproject.toml b/pyproject.toml index 419b636a9a6..3ac799fec8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ django-simple-captcha = "^0.5.14" uWSGI = "^2.0.20" newrelic = "^7.2.4" psycopg = {extras = ["binary"], version = "^3.1.10"} +redis = {version = "^5.0.0", extras = ["hiredis"]} [tool.poetry.group.docs] optional = true From c5777ee69592b89a9ab13c15e3e9b4ba16d97d72 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 22 Sep 2023 15:41:06 +0300 Subject: [PATCH 2/7] added installation of redis-server package in CI Signed-off-by: Alexander Piskun --- .github/workflows/yarn.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yarn.yml b/.github/workflows/yarn.yml index 74054c505f1..21373843727 100644 --- a/.github/workflows/yarn.yml +++ b/.github/workflows/yarn.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install dependencies - run: sudo apt install gettext netcat-openbsd xvfb + run: sudo apt install gettext netcat-openbsd xvfb redis-server - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 18e479a65164b48e637139ce84c09fa92201f68b Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 22 Sep 2023 16:21:59 +0300 Subject: [PATCH 3/7] adjusted cache TTL values Signed-off-by: Alexander Piskun --- nextcloudappstore/api/v1/response_redis_cache.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nextcloudappstore/api/v1/response_redis_cache.py b/nextcloudappstore/api/v1/response_redis_cache.py index f568e245b59..8e68e38459c 100644 --- a/nextcloudappstore/api/v1/response_redis_cache.py +++ b/nextcloudappstore/api/v1/response_redis_cache.py @@ -21,8 +21,7 @@ def decorator(func): @wraps(func) def inner(request, *args, **kwargs): # The value from etag_func() could be quoted or unquoted. - res_etag = etag_func(request, *args, **kwargs) if etag_func else None - res_etag = quote_etag(res_etag) if res_etag is not None else None + res_etag = quote_etag(etag_func(request, *args, **kwargs)) response = get_conditional_response( request, @@ -34,12 +33,14 @@ def inner(request, *args, **kwargs): if not cached_response: response = func(request, *args, **kwargs) if response.status_code == 200: - cache.set(res_etag, response.rendered_content, 10 * 60) + # apps.json size > 12 MB, to store 100 caches we need 1.2 GB + # let assume we don't get more than 100 release updates in 3 minutes. + cache.set(res_etag, response.rendered_content, 180) else: + cache.touch(res_etag, 90) response = HttpResponse(content=cached_response, content_type="application/json") if request.method in ("GET", "HEAD"): - if res_etag: - response.headers.setdefault("ETag", res_etag) + response.headers.setdefault("ETag", res_etag) return response return inner From 88d19e3119f4d6ddb9f39fed642a237155eb549a Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 22 Sep 2023 19:10:34 +0300 Subject: [PATCH 4/7] added support for multiply endpoints, corrected cache key names. Signed-off-by: Alexander Piskun --- nextcloudappstore/api/v1/response_redis_cache.py | 15 ++++++--------- nextcloudappstore/api/v1/urls.py | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/nextcloudappstore/api/v1/response_redis_cache.py b/nextcloudappstore/api/v1/response_redis_cache.py index 8e68e38459c..cb0c95b72c6 100644 --- a/nextcloudappstore/api/v1/response_redis_cache.py +++ b/nextcloudappstore/api/v1/response_redis_cache.py @@ -6,11 +6,7 @@ from django.utils.http import quote_etag -def etag_redis(etag_func): - return condition_redis(etag_func) - - -def condition_redis(etag_func): +def etag_redis(etag_func, key_prefix: str): """Cache of reply for HTTP request in Redis, where Redis Key is the ETag of reply. .. note:: It is the dev version, and can be used only for the one endpoint @@ -29,15 +25,16 @@ def inner(request, *args, **kwargs): ) if response is None: cache = caches["default"] - cached_response = cache.get(res_etag) + redis_key = key_prefix + res_etag.replace(" ", "_").replace('"', "") + cached_response = cache.get(redis_key) if not cached_response: response = func(request, *args, **kwargs) if response.status_code == 200: # apps.json size > 12 MB, to store 100 caches we need 1.2 GB - # let assume we don't get more than 100 release updates in 3 minutes. - cache.set(res_etag, response.rendered_content, 180) + # let assume we don't get more than 100 release updates in 2.5 minutes. + cache.set(redis_key, response.rendered_content, 150) else: - cache.touch(res_etag, 90) + cache.touch(redis_key, 90) response = HttpResponse(content=cached_response, content_type="application/json") if request.method in ("GET", "HEAD"): response.headers.setdefault("ETag", res_etag) diff --git a/nextcloudappstore/api/v1/urls.py b/nextcloudappstore/api/v1/urls.py index 60b9ab6a98d..5cafd357dbe 100644 --- a/nextcloudappstore/api/v1/urls.py +++ b/nextcloudappstore/api/v1/urls.py @@ -1,7 +1,7 @@ from django.urls import re_path from django.views.decorators.http import etag -from nextcloudappstore.api.v1.response_redis_cache import condition_redis +from nextcloudappstore.api.v1.response_redis_cache import etag_redis from nextcloudappstore.api.v1.views import ( AppRatingView, AppRegisterView, @@ -27,7 +27,7 @@ urlpatterns = [ re_path(r"^platforms\.json$", etag(nextcloud_release_etag)(NextcloudReleaseView.as_view()), name="platforms"), re_path(r"^platform/(?P\d+\.\d+\.\d+)/apps\.json$", etag(apps_etag)(AppView.as_view()), name="app"), - re_path(r"^apps\.json$", condition_redis(apps_all_etag)(AppsView.as_view()), name="apps"), + re_path(r"^apps\.json$", etag_redis(apps_all_etag, "apps_")(AppsView.as_view()), name="apps"), re_path(r"^apps/releases/?$", AppReleaseView.as_view(), name="app-release-create"), re_path(r"^apps/?$", AppRegisterView.as_view(), name="app-register"), re_path(r"^apps/(?P[a-z0-9_]+)/?$", AppView.as_view(), name="app-delete"), From 27b06f37345de6d9987fa6f457dde46238e19242 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Sat, 23 Sep 2023 01:05:03 +0300 Subject: [PATCH 5/7] replaced previous non-success cache try with `last_modified` header support Signed-off-by: Alexander Piskun --- .../api/v1/response_redis_cache.py | 45 ------------------- nextcloudappstore/api/v1/urls.py | 20 ++++++--- nextcloudappstore/core/caching.py | 31 ++++++++++++- 3 files changed, 45 insertions(+), 51 deletions(-) delete mode 100644 nextcloudappstore/api/v1/response_redis_cache.py diff --git a/nextcloudappstore/api/v1/response_redis_cache.py b/nextcloudappstore/api/v1/response_redis_cache.py deleted file mode 100644 index cb0c95b72c6..00000000000 --- a/nextcloudappstore/api/v1/response_redis_cache.py +++ /dev/null @@ -1,45 +0,0 @@ -from functools import wraps - -from django.core.cache import caches -from django.http.response import HttpResponse -from django.utils.cache import get_conditional_response -from django.utils.http import quote_etag - - -def etag_redis(etag_func, key_prefix: str): - """Cache of reply for HTTP request in Redis, where Redis Key is the ETag of reply. - - .. note:: It is the dev version, and can be used only for the one endpoint - if all goes well it will be modified to support multiply endpoints. - """ - - def decorator(func): - @wraps(func) - def inner(request, *args, **kwargs): - # The value from etag_func() could be quoted or unquoted. - res_etag = quote_etag(etag_func(request, *args, **kwargs)) - - response = get_conditional_response( - request, - etag=res_etag, - ) - if response is None: - cache = caches["default"] - redis_key = key_prefix + res_etag.replace(" ", "_").replace('"', "") - cached_response = cache.get(redis_key) - if not cached_response: - response = func(request, *args, **kwargs) - if response.status_code == 200: - # apps.json size > 12 MB, to store 100 caches we need 1.2 GB - # let assume we don't get more than 100 release updates in 2.5 minutes. - cache.set(redis_key, response.rendered_content, 150) - else: - cache.touch(redis_key, 90) - response = HttpResponse(content=cached_response, content_type="application/json") - if request.method in ("GET", "HEAD"): - response.headers.setdefault("ETag", res_etag) - return response - - return inner - - return decorator diff --git a/nextcloudappstore/api/v1/urls.py b/nextcloudappstore/api/v1/urls.py index 5cafd357dbe..0bdad0b1f33 100644 --- a/nextcloudappstore/api/v1/urls.py +++ b/nextcloudappstore/api/v1/urls.py @@ -1,7 +1,6 @@ from django.urls import re_path -from django.views.decorators.http import etag +from django.views.decorators.http import condition, etag -from nextcloudappstore.api.v1.response_redis_cache import etag_redis from nextcloudappstore.api.v1.views import ( AppRatingView, AppRegisterView, @@ -16,8 +15,11 @@ from nextcloudappstore.core.caching import ( app_ratings_etag, apps_all_etag, + apps_all_last_modified, apps_etag, + apps_last_modified, categories_etag, + categories_last_modified, nextcloud_release_etag, ) from nextcloudappstore.core.versioning import SEMVER_REGEX @@ -26,8 +28,12 @@ urlpatterns = [ re_path(r"^platforms\.json$", etag(nextcloud_release_etag)(NextcloudReleaseView.as_view()), name="platforms"), - re_path(r"^platform/(?P\d+\.\d+\.\d+)/apps\.json$", etag(apps_etag)(AppView.as_view()), name="app"), - re_path(r"^apps\.json$", etag_redis(apps_all_etag, "apps_")(AppsView.as_view()), name="apps"), + re_path( + r"^platform/(?P\d+\.\d+\.\d+)/apps\.json$", + condition(apps_etag, apps_last_modified)(AppView.as_view()), + name="app", + ), + re_path(r"^apps\.json$", condition(apps_all_etag, apps_all_last_modified)(AppsView.as_view()), name="apps"), re_path(r"^apps/releases/?$", AppReleaseView.as_view(), name="app-release-create"), re_path(r"^apps/?$", AppRegisterView.as_view(), name="app-register"), re_path(r"^apps/(?P[a-z0-9_]+)/?$", AppView.as_view(), name="app-delete"), @@ -39,5 +45,9 @@ ), re_path(r"^token/?$", SessionObtainAuthToken.as_view(), name="user-token"), re_path(r"^token/new/?$", RegenerateAuthToken.as_view(), name="user-token-new"), - re_path(r"^categories.json$", etag(categories_etag)(CategoryView.as_view()), name="category"), + re_path( + r"^categories.json$", + condition(categories_etag, categories_last_modified)(CategoryView.as_view()), + name="category", + ), ] diff --git a/nextcloudappstore/core/caching.py b/nextcloudappstore/core/caching.py index 5f5df5acf09..d15afcfbcd3 100644 --- a/nextcloudappstore/core/caching.py +++ b/nextcloudappstore/core/caching.py @@ -1,4 +1,5 @@ -from typing import Any, List, Tuple +import datetime +from typing import Any, List, Tuple, Union from django.db.models import Max, QuerySet from semantic_version import Version @@ -24,6 +25,12 @@ def create_etag(pairs: List[Tuple[QuerySet, str]]) -> str: return str(max(result, default="")) +def get_last_modified(pairs: List[Tuple[QuerySet, str]]) -> Union[datetime.datetime, None]: + result = map(lambda p: p[0].aggregate(m=Max(p[1]))["m"], pairs) + result = filter(lambda r: r is not None, result) + return max(result, default=None) + + def apps_etag(request: Any, version: str) -> str: return create_etag( [ @@ -33,6 +40,15 @@ def apps_etag(request: Any, version: str) -> str: ) +def apps_last_modified(request: Any, version: str) -> Union[datetime.datetime, None]: + return get_last_modified( + [ + (App.objects.all(), "last_release"), + (AppReleaseDeleteLog.objects.all(), "last_modified"), + ] + ) + + def apps_all_etag(request: Any) -> str: return create_etag( [ @@ -42,6 +58,15 @@ def apps_all_etag(request: Any) -> str: ) +def apps_all_last_modified(request: Any) -> Union[datetime.datetime, None]: + return get_last_modified( + [ + (App.objects.all(), "last_release"), + (AppReleaseDeleteLog.objects.all(), "last_modified"), + ] + ) + + def app_etag(request: Any, id: str) -> str: return str(App.objects.get(id=id).last_modified) @@ -54,6 +79,10 @@ def categories_etag(request: Any) -> str: return create_etag([(Category.objects.all(), "last_modified")]) +def categories_last_modified(request: Any) -> Union[datetime.datetime, None]: + return get_last_modified([(Category.objects.all(), "last_modified")]) + + def app_ratings_etag(request: Any) -> str: return create_etag([(AppRating.objects.all(), "rated_at")]) From 2132c169e1c2af1dd697cfd992a7ec90547c9545 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Sat, 23 Sep 2023 14:00:22 +0300 Subject: [PATCH 6/7] api/v1/apps.json endpoint should return gzipped content to nginx Signed-off-by: Alexander Piskun --- .gitignore | 1 + nextcloudappstore/api/v1/views.py | 3 +++ nextcloudappstore/settings/base.py | 7 ------- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 93425f43535..d215f4a226f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ newrelic.ini /config .DS_Store /nextcloudappstore/static +/dev/ diff --git a/nextcloudappstore/api/v1/views.py b/nextcloudappstore/api/v1/views.py index a3c06e36380..818b0de0ec4 100644 --- a/nextcloudappstore/api/v1/views.py +++ b/nextcloudappstore/api/v1/views.py @@ -2,6 +2,8 @@ from django.conf import settings from django.db import transaction from django.http import Http404 +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page from pymple import Container from requests import HTTPError from rest_framework import authentication, parsers, renderers # type: ignore @@ -66,6 +68,7 @@ class NextcloudReleaseView(ListAPIView): serializer_class = NextcloudReleaseSerializer +@method_decorator(gzip_page, name="dispatch") class AppsView(ListAPIView): queryset = App.objects.prefetch_related(*APP_PREFETCH_LIST).all() serializer_class = AppSerializer diff --git a/nextcloudappstore/settings/base.py b/nextcloudappstore/settings/base.py index cb8bc8f2274..6e9b034330b 100644 --- a/nextcloudappstore/settings/base.py +++ b/nextcloudappstore/settings/base.py @@ -357,10 +357,3 @@ # Account login bruteforce ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 10 ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 3600 # 1 hour in seconds - -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.redis.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/", - } -} From 3e0039b576575abd07e97c3f9652a4af4d682284 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Sat, 23 Sep 2023 17:18:37 +0300 Subject: [PATCH 7/7] updated CHANGELOG.md Signed-off-by: Alexander Piskun --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762b6af0df7..27c2142f6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] +## [4.3.2] - 2023-09-23 + +### Added + +- Support of `Last-Modified`, e.g. `If-Modified-Since:` header for the Rest API endpoints. #1147 +- `redis` python dependency for `django-allauth`. + +### Changed + +- `/api/v1/apps.json` endpoint now ALWAYS return gzipped data. #1147 + ## [4.3.1] - 2023-09-19 ### Changed