Skip to content

enforce a daily download limit#2160

Merged
alastair merged 2 commits into
masterfrom
daily-download-limit
Jul 14, 2026
Merged

enforce a daily download limit#2160
alastair merged 2 commits into
masterfrom
daily-download-limit

Conversation

@alastair

Copy link
Copy Markdown
Member

Description
Very infrequently we see people try to download all of Freesound via the download buttons on each sound. Stop this from happening.

Count the number of downloads that a user makes over a day. Align all users to UTC for simplicity. Use Redis to store the count. Count Sounds, Packs, Collections, and Bookmark Category downloads as 1 download each time.
Once a user goes over the limit, don't show a download link, and prevent the direct download link from providing the file (HTTP 429).

Set an "in progress" download key when the user starts a download, and don't count again if they access the URL when the download sentinel is active - this prevents download managers that request different ranges from counting multiple times.

Add the same guard to Collection downloads (it was missing)

The In progress download key in redis was previously set, but we also checked the http Range header to see if this was a "new" or "continued" download. Remove the Range header entirely, because the redis check performs pretty much the same check and we were using it anyway.

There was one bug where a user could maliciously request Range: 0- in order to circumvent the check, although we expect that this probably never happened.
The new check does mean that if there is a large file (takes longer than 5 minutes to download) and the client makes multiple requests to download it, that we'll count this twice. We'll check this in a few weeks but don't expect that it'll cause an issue either.

Deployment steps:
Requires the "abuse" cache to be added in prod. We'll add it to volatile.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements a Redis-backed per-user daily download limit to mitigate abusive “download everything” behavior, including UI hiding of download links and server-side enforcement with HTTP 429 responses.

Changes:

  • Add utils.download_limit helpers to count daily downloads (UTC day) in Redis and deduplicate multi-request downloads via a short-lived sentinel.
  • Enforce the limit in sound, pack, collection, and bookmark-category download endpoints (429) and hide/replace download affordances in templates (modal + disabled-looking UI).
  • Add Redis-requiring tests plus CI/test-environment updates (Redis service, settings, pytest marker).

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
utils/tests/test_download_limit.py Unit tests for Redis daily counter behavior and fail-open semantics.
utils/download_limit.py Core daily counter + “in progress” sentinel logic and 429 response helper.
templates/sounds/sound.html Hide direct download link when over limit; show modal trigger instead.
templates/sounds/pack.html Same as above for pack downloads.
templates/sounds/download_limit_reached.html New 429 page template.
templates/molecules/modal_download_limit.html New modal template explaining the limit.
templates/collections/your_collections.html Pass limit state into collection card rendering.
templates/collections/display_collection.html Hide collection download link on cards when over limit.
templates/collections/collection.html Disable collection download button and show modal trigger when over limit.
templates/bookmarks/bookmarks.html Disable bookmark-category download icon and show modal trigger when over limit.
sounds/views.py Apply guard + dedup logic for sound/pack downloads; add modal view; pass context flag.
sounds/tests/test_sound.py Ensure on-commit callbacks execute so sentinel/count logic works in tests.
sounds/tests/test_download_limit_views.py End-to-end tests for 429 behavior and link hiding across pages.
pyproject.toml Register redis pytest marker.
fscollections/views.py Apply guard + dedup logic for collection downloads; pass context flag.
fscollections/templatetags/display_collections.py Accept download_limit_reached input and forward to template.
freesound/urls.py Add route for the download-limit modal.
freesound/test_settings.py Add Redis-backed abuse cache for tests.
freesound/settings.py Add abuse cache + default daily limit settings/message.
docker-compose.test.yml Add Redis service for test stack.
bookmarks/views.py Apply guard + dedup logic for bookmark-category downloads; pass context flag.
.github/workflows/unit-tests.yml Pull Redis image in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread utils/download_limit.py
Comment thread utils/download_limit.py
Comment thread templates/sounds/sound.html
Comment on lines +93 to +95
{% if download_limit_reached %}
<a class="no-hover btn-primary display-inline-block w-100 text-center opacity-050 cursor-pointer" data-toggle="modal-default" data-modal-content-url="{% url 'download-limit-modal' %}?ajax=1" title="Daily download limit reached">Download pack</a>
{% else %}
Comment thread templates/collections/collection.html
Comment thread templates/bookmarks/bookmarks.html

@ffont ffont left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New logic looks much cleaner in code. I added a couple of relevant comments though :)

Comment thread utils/download_limit.py


def download_limit_reached(user_id):
return get_daily_download_count(user_id) >= settings.MAX_DOWNLOADS_PER_DAY

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we skip download limits for sound uploaders and possibly some other cases? Although you already mentioned the threshold is set well below any "human" download activity

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably suggest that we leave this one for now and add another issue for it, because as we said it should never happen (99% of downloads in the last year were < 50/user/month and our limit is 200/day)

Comment thread fscollections/templatetags/display_collections.py Outdated
Comment thread templates/collections/display_collection.html Outdated
@alastair alastair force-pushed the daily-download-limit branch 2 times, most recently from 9aacf67 to d9e4e15 Compare July 14, 2026 08:00
alastair added 2 commits July 14, 2026 10:06
This link should open the modal, not trigger a download
Count the number of downloads that a user makes over a day. Align all users
to UTC for simplicity. Use Redis to store the count.
Count Sounds, Packs, Collections, and Bookmark Category downloads as 1
download each time.
Once a user goes over the limit, don't show a download link, and
prevent the direct download link from providing the file (HTTP 429).

Set an "in progress" download key when the user starts a download, and don't
count again if they access the URL when the download sentinel is active -
this prevents download managers that request different ranges from counting
multiple times.

Add the same guard to Collection downloads (it was missing)
@alastair alastair force-pushed the daily-download-limit branch from d9e4e15 to 8074fe9 Compare July 14, 2026 08:06
@alastair alastair merged commit 8074fe9 into master Jul 14, 2026
3 checks passed
@alastair alastair deleted the daily-download-limit branch July 14, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants