mirror of
https://github.com/suitenumerique/drive.git
synced 2026-08-17 20:15:40 +02:00
🐛(backend) exclude hard-deleted items from storage computation
Hard-deleted items no longer occupy storage but still counted in the creator's usage, inflating the quota gauge and blocking uploads for users who had cleaned up their trash. The new partial covering index keeps the per-creator sum an index-only scan now that the quota check runs it on every uncached upload.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.14 on 2026-07-08 10:14
|
||||
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
# CREATE INDEX CONCURRENTLY cannot run inside a transaction. It avoids
|
||||
# locking writes on the item table while the index is being built.
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('core', '0025_user_storage_limit_override'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name='item',
|
||||
index=models.Index(condition=models.Q(('hard_deleted_at__isnull', True)), fields=['creator'], include=('size',), name='item_creator_size_not_hdel_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1034,6 +1034,13 @@ class Item(TreeModel, BaseModel):
|
||||
indexes = [
|
||||
GistIndex(fields=["path"]),
|
||||
models.Index(NLevel(models.F("path")), name="drive_item_path_nlevel_idx"),
|
||||
# Covers the storage used computation by creator.
|
||||
models.Index(
|
||||
fields=["creator"],
|
||||
include=["size"],
|
||||
condition=models.Q(hard_deleted_at__isnull=True),
|
||||
name="item_creator_size_not_hdel_idx",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -15,6 +15,6 @@ class CreatorStorageComputeBackend(StorageComputeBackend):
|
||||
"""
|
||||
Compute the total storage used by a set of users.
|
||||
"""
|
||||
return Item.objects.filter(creator__in=users).aggregate(total_size=Sum("size", default=0))[
|
||||
"total_size"
|
||||
]
|
||||
return Item.objects.filter(creator__in=users, hard_deleted_at__isnull=True).aggregate(
|
||||
total_size=Sum("size", default=0)
|
||||
)["total_size"]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Tests for the CreatorStorageComputeBackend.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
from core.storage.creator_storage_compute_backend import CreatorStorageComputeBackend
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_compute_storage_used_sums_creator_items():
|
||||
"""The backend should sum the sizes of all items created by the given users."""
|
||||
user = factories.UserFactory()
|
||||
factories.ItemFactory(creator=user, size=100)
|
||||
factories.ItemFactory(creator=user, size=250)
|
||||
factories.ItemFactory(size=999) # another creator, should not count
|
||||
|
||||
assert CreatorStorageComputeBackend().compute_storage_used([user]) == 350
|
||||
|
||||
|
||||
def test_compute_storage_used_excludes_hard_deleted_items():
|
||||
"""Hard-deleted items should not count toward the storage used."""
|
||||
user = factories.UserFactory()
|
||||
factories.ItemFactory(creator=user, size=100)
|
||||
hard_deleted = factories.ItemFactory(creator=user, size=250)
|
||||
hard_deleted.soft_delete()
|
||||
hard_deleted.hard_delete()
|
||||
|
||||
assert CreatorStorageComputeBackend().compute_storage_used([user]) == 100
|
||||
|
||||
|
||||
def test_compute_storage_used_keeps_soft_deleted_items():
|
||||
"""Soft-deleted (trashbin) items should still count toward the storage used."""
|
||||
user = factories.UserFactory()
|
||||
factories.ItemFactory(creator=user, size=100)
|
||||
soft_deleted = factories.ItemFactory(creator=user, size=250)
|
||||
soft_deleted.soft_delete()
|
||||
|
||||
assert CreatorStorageComputeBackend().compute_storage_used([user]) == 350
|
||||
Reference in New Issue
Block a user