Commit Graph
1409 Commits
Author SHA1 Message Date
renovate[bot]andGitHub 4c07fdad1d ⬆️(dependencies) update python dependencies 2026-10-12 02:23:48 +00:00
Nicolas Clerc 0b0c63af31 🔧(helm) relax the backend readiness probe
The readiness probe runs every Django check and opens a database
connection. With a 1s timeout every 10s, a slow database took the
pods out of the Service. Give it more time and call it less often.
Fix the probe rows in the README.
2026-10-09 13:49:21 +02:00
Nathan VasseandNicolas Clerc 2c6c1a9e48 ✨(backend) show the full quota of users in the admin
The quota field only handled the usage and limit shape of the quota. The
other ones, like an error or a locked organization, were shown as raw
values without labels and an absent quota was not mentioned at all. Every
field of the quota is now labelled, along with the storage counted by
Drive as the backend may compute the usage on its own.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 331888d522 ✨(backend) set the storage limit override of users in GB in the admin
The override is stored in bytes, which is error prone to type by hand
for limits counted in gigabytes.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 071bcb6db2 ♻️(frontend) format upload sizes with formatSize
The upload toast used pretty-bytes, rounding and labelling sizes
differently from the rest of the app. Use formatSize and drop the
dependency.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 4d01537e7b ✨(backend) show user storage quota and biggest files in the admin
Checking why a user hits the quota required database access. Show the
storage counted in the quota and the effective limit of each user, and
the biggest files in the user form, with a selectable count. Admin
sizes now use format_size to match the app.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 195c34c098 ♻️(backend) expose the storage used as a User queryset expression
The user admin needs the storage used of each user to list and sort
them, so the backend provides it to keep a single item filter.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 4f7270eb0a ♻️(backend) add a format_size helper matching the frontend
The admin formats sizes in base 1024 with Django filesizeformat while
the app and the storage quota use decimal units. Mirror the frontend
formatSize, rounding included, so both sides show the same values.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 675632b61c ✨(backend) return the quota exclusion of items in the API
Clients need to know which items do not count in the storage quota.
The field stays read only until editing it gets its own permission.
2026-10-09 13:06:14 +02:00
Nicolas Clerc 06a2148713 ✨(backend) manage the quota exclusion of items in the admin
quota_excluded could not be checked nor fixed without database access.
Show it with the item size in the list, filter on it, and let admins
toggle it on items and their descendants, from the list or the form.
2026-10-09 13:05:59 +02:00
Nicolas Clerc b0fb79f89a ✨(backend) set the quota exclusion of an item and its descendants
Excluding a folder from the storage quota must cover its whole tree.
The bulk update bypasses Item.save(), so the storage used cache of
every impacted creator is invalidated explicitly.
2026-10-09 13:05:59 +02:00
Nicolas Clerc 0210f728dd ✨(wopi) capture a posthog event on document edition
Emit item_edited when the WOPI client saves a document. Anonymous
editors through a public link have no email, so no distinct id is sent.
2026-10-09 12:21:42 +02:00
Nicolas Clerc 7145c52018 ✨(backend) capture a posthog event on folder export
Emit item_exported when a folder archive is requested to measure the
usage of the export feature.
2026-10-09 12:21:42 +02:00
Nicolas Clerc 79b4759177 ✨(backend) capture a posthog event on conversion request
Emit item_converted when the conversion is queued rather than when the
celery task completes, to keep the event tied to the user action.
2026-10-09 12:21:42 +02:00
Nicolas Clerc 0cdfe46b88 ✨(backend) capture posthog events on item deletion
Emit item_deleted on soft delete and item_hard_deleted on the
hard-delete action to measure how often users empty their trash.
Detaching a restriction is not a deletion and stays silent.
2026-10-09 12:21:42 +02:00
Nicolas Clerc fea281cf32 ✨(backend) capture a posthog event on template based creation
Documents created from a template never go through the upload flow, so
the item_uploaded event misses them. Emit item_created_from_template
from the shared helper to cover both root and children creation.
2026-10-09 12:21:42 +02:00
Nicolas Clerc c68a06cc22 🔒️(backend) serve WOPI file content as a sandboxed download
File content was streamed inline on the application origin, with no
fallback when the item has no mimetype. WOPI clients fetch it server
side, so serving it as a sandboxed download costs them nothing and
keeps stored bytes from ever rendering in a browser.
2026-10-08 17:25:29 +02:00
Nathan Vasse 75fd41fce7 ✨(backend) schedule management commands with celery beat
The trash purge and the stale pending items cleaning only ran through
the cron jobs of the Helm chart, so any other deployment had to recreate
them or never clean anything. Celery beat already runs next to the app,
so it can schedule them wherever Drive is deployed.

Each command has its own setting holding a crontab line followed by the
command arguments, so operators can tune both in one place. They are
empty by default: scheduling a command that already runs as a cron job
would run it twice. They are validated at startup, arguments included,
because celery only logs errors raised while registering periodic tasks
and a typo would then silently never run the command.
2026-10-08 15:45:12 +02:00
Nathan Vasse bd21d84c6e 🐛(backend) purge the stored object of stale pending items
The command deleted the row of an abandoned upload but left its object
in the storage. The row being the only pointer to that object, nothing
could find it afterwards and it stayed there forever, out of any quota.

Stale pending items are now only marked as hard deleted, leaving the
purge_deleted_items command remove the object then the row as it does
for any other item. An upload that never reached the storage has no
object to remove, which the purge already tolerates.
2026-10-08 14:59:59 +02:00
Nathan Vasse 6595c9da15 🐛(backend) do not clean a pending item whose upload just ended
The command listed the stale pending items then deleted them one by one
without checking again. An upload ended between the listing and the
deletion was still removed, along with its finalized object reference.

Each item is now locked and its pending state checked again before it is
deleted.
2026-10-08 14:59:59 +02:00
Nathan Vasse 0b484b9355 ⚡️(backend) count the accesses of listed children in a subquery
The number of accesses of each child was counted by joining the
accesses, which read the whole access table and grouped the query. A
grouped query cannot be counted directly, so the pagination count
wrapped the whole query, computing again every annotation of every
child: 294ms of the children listing of a large production folder.

Counting the direct accesses in a correlated subquery returns the same
numbers without grouping the query, and the pagination count becomes a
plain count of the children.
2026-10-08 14:27:11 +02:00
Nathan Vasse 99179f1a73 ⚡️(backend) look the children of an item up by their parent path
The children of an item were found among all its descendants, then
filtered on their depth: listing the 9 children of a production folder
holding 62k items read the whole subtree through the GiST index, 1.2s,
and counting the children of each of them read their own subtrees,
another 0.8s.

The item table is now indexed on the path of each item's parent, so
that both are exact lookups returning only the children: 0.7ms and
4.6ms on the same folder. The index is built concurrently, about 20
seconds and 900MB for 4M items, and matches the previous definition of
children for every item, including those whose path is inconsistent.
2026-10-08 14:27:11 +02:00
Nathan Vasse 834217f89a 📝(backend) document how to check the scaling of item queries
Queries that are fast on a development database can take seconds on an
instance with millions of items, and nothing explained how to check a
change on realistic data.

This page explains how to generate a production sized dataset and
profile the endpoints, why JIT should be disabled on production
databases, and why the GiST index on paths should be avoided in queries
that run for every request or every item.
2026-10-08 14:27:11 +02:00
Nathan Vasse d35d4c291d ⚡️(backend) read the number of accesses of listed items at once
Each listed item read its number of accesses from the cache with its
own round trip: 350ms for a page of 200 items.

The item list serializer now reads them all with a single get_many
before serializing the items, when its fields include the number of
accesses. Items missing from the cache still compute it when it is
read, through the permissions backend as before.
2026-10-08 14:27:11 +02:00
Nathan Vasse cb3c3ebbc9 ⚡️(backend) fetch the WOPI configuration once per item list
To tell whether each listed file can be opened with WOPI, the serializer
fetched the whole WOPI configuration from the cache, file by file. The
configuration is large and unpickling it for every file was most of the
serialization time: 315ms for a page of 200 items.

It is now fetched once per serialization and passed down to the WOPI
support check. Other callers keep fetching it themselves.
2026-10-08 14:27:11 +02:00
Nathan Vasse d8faa7040c ⚡️(backend) count children with a range of the path index
The number of children and of folder children of each listed item is
counted by two subqueries, each one scanning the GiST index of paths to
find all the descendants of the item: about 1ms per subquery on a large
database, so 2 x 200 scans for a page of items.

The descendants are now found with a new path__in_subtree lookup, a
faster equivalent of path__descendants: it reads the subtree as a range
of the unique btree index on paths, like subtrees in recents, and
returns the same rows about 20 times faster. The depth is kept as a
difference with the outer item: with an equality on the depth, Postgres
would combine the path range with the depth index, matching a whole
level of all the trees.
2026-10-08 14:27:11 +02:00
Nathan Vasse a1cda84140 ⚡️(backend) look subtrees up as path ranges in recents and favorites
Recents and favorites filter the items under the roots the user can
reach with one "path is a descendant of this root" condition per root,
OR-ed together. Postgres resolved them with one GiST index scan per
root: about 1s for a user reaching 1700 roots, paid twice with the
pagination count.

ltree compares labels byte by byte and the hyphen is the lowest byte
allowed in a label, so the subtree of "a.b", and only it, sorts between
"a.b" and "a.b-". Each subtree is thus a range of the unique btree index
on paths. The ids of all the subtrees are computed at once from these
ranges, then items are looked up by primary key: 35ms instead of 1s for
the same user.
2026-10-08 14:27:10 +02:00
Nathan Vasse f00de91296 ⚡️(backend) give the ancestors links to the item list serializer
Unlike recents and favorites, the item list did not give the ancestors
links of its items to its serializer. Each listed item that is not at
the root of its tree, like a folder shared from inside another tree,
computed them with its own query when computing its abilities: 66
queries for a user reaching many shared folders.

Now that they are computed in one query for all the items, the list
computes them too.
2026-10-08 14:27:10 +02:00
Nathan Vasse 0afdcce085 ⚡️(backend) compute the ancestors links of many items in one query
The ancestors links of listed items, and of the items traced by link
when filtering accessible items, were computed with one query per tree:
more than 200 queries for a user with many link traces, each one an
ancestors lookup on the GiST index of paths.

The paths of all the ancestors are known from the items paths, so they
are now fetched at once through the unique index on paths. The mapping
only covers the parents of the items and their ancestors, the only paths
it is read at, so that root items need no query at all.
2026-10-08 14:27:10 +02:00
Nathan Vasse a7966c303a ⚡️(backend) avoid a full table scan when filtering accessible items
Accessible items were filtered with "id in the accesses of the user OR
id in the items traced by link". Postgres cannot resolve such an OR
with an index and scanned the whole item table instead, as soon as the
user had one link trace: about 1s on every list, recents and favorites
request with 4M items, whatever the number of items of the user.

Merging both sets in a single UNION subquery lets Postgres look items
up by primary key: the same query now takes a few milliseconds.
2026-10-08 14:27:10 +02:00
Nathan Vasse 79ce036faf ⚡️(backend) look inherited accesses up by the ids in the item path
effective_accesses and roles_at found the accesses applying to an item
with an ancestors lookup on the item paths, resolved on their GiST index
at about 10ms per item. Counting the accesses of 200 items took 2.4s,
which happens when their number of accesses is missing from the cache.

Filtering accesses on the ids found in the item path uses the access
item index instead and returns the same accesses: the same 200 counts
now take 85ms.
2026-10-08 14:27:10 +02:00
Nathan Vasse 563f87a34d ⚡️(backend) check other ancestor accesses from the item path ids
The trashbin owner filter, the reachable restrictions hidden from the
item list and the contact filter used the same join on the ancestors of
each item as the user roles did, with the same cost growing with the
number of accesses or link traces of the user. They now rely on IdInPath
to look accesses, link traces and ancestors up by id.
2026-10-08 14:27:10 +02:00
Nathan Vasse 22fa10111b ⚡️(backend) resolve user roles from the ids in the item path
The user roles subquery joined the accesses of the user on their items
and kept those whose path is an ancestor of the listed item. Postgres
went through all the accesses of the user for each listed item: a user
holding 1500 accesses and listing 200 items meant 305k index lookups and
more than 800ms.

An item path is made of the ids of its ancestors and its own id, so the
accesses applying to an item are those whose item id is one of its path
labels. The IdInPath expression looks them up on the access index
directly, without joining items: the same page now takes 5ms.
2026-10-08 14:27:10 +02:00
Nathan Vasse 7bd77badc8 ✨(backend) add a command to profile the main API endpoints
Measuring the effect of query changes needs more than the query counts
asserted by tests. This command calls the item and user endpoints as a
given user and reports their response times, their queries and,
optionally, the execution plan of their slowest query. The JIT can be
disabled for the session to measure its cost, and the endpoints can be
narrowed down to iterate faster.
2026-10-08 14:27:10 +02:00
Nathan Vasse 544f47f9b7 ✨(backend) add a command to generate a production sized dataset
Slow queries reported in production could not be reproduced locally:
the demo command only creates a handful of items through factories.

This command fills an empty database with a dataset shaped like
production: 50k users, 4M items spread over deep trees, accesses,
link traces, favorites and trashed items. Rows are generated with set
based SQL so that it runs in minutes, and no file is written to the
object storage. The development user gets a small drive to reproduce
slow requests for users with few items.
2026-10-08 14:27:10 +02:00
Nathan Vasse 311265dc79 🔥(frontend) remove hardcoded siret from anct gaufre url
The La Gaufre services url of the anct theme carried the siret of a
single organization, so every user was served the services list of
that organization. The operator alone is enough to scope the request.
The cunningham tokens are regenerated accordingly.
2026-10-02 16:52:52 +02:00
Nicolas Clerc 90fd275eb2 ⬆️(backend) update urllib3
Bump the transitive urllib3 to 2.8.0 to close security advisories.
2026-10-01 11:43:10 +02:00
Nathan Vasse 2bd1f4267d ✅(e2e) scope the trash confirm click to the dialog
The selection bar button and the modal confirm button share the same
name. When the locator resolved before the modal was rendered, it kept
targeting the bar button hidden behind the backdrop and the click timed
out. It made the chromium shard fail on unrelated branches.
2026-10-01 11:13:13 +02:00
Nathan Vasse ce73f5f493 ✅(backend) keep the per-file limit test above the request body size
Django applies DATA_UPLOAD_MAX_MEMORY_SIZE to the request body too, so a
10 bytes limit rejected the JSON payload of the test itself before the
serializer could check the reserved size. The CI failed on it.
2026-10-01 10:43:48 +02:00
Nathan Vasse a03c2f8e13 🐛(backend) keep cleaning stale pending items when one fails
A pending item already in the trash, directly or through an ancestor,
made soft_delete raise and aborted the whole command, leaving every
following item in place. Each item is now cleaned in its own
transaction and a failure is logged and counted instead of stopping
the run.
2026-10-01 09:54:11 +02:00
Nathan Vasse b4f8825481 ✨(backend) rate limit the item creation endpoints
Creating, adding a child and duplicating now share a per-user burst and
sustained budget, whatever the item type, so creation is bounded in rate as
well as in volume.

Both rates are settings so an operator can tune them, and the defaults sit
well above what the interface produces: uploads are serial and a folder drop
sends one request per entry, so a real user never reaches them.
2026-10-01 09:54:11 +02:00
Nathan Vasse cec2fe1a0e 📝(docs) declare the file size in the resource server example
Integrators copying this snippet would now get a 400: creating a file item
requires a size reservation, and the returned policy signs Content-Length,
so the raw file must be sent rather than a multipart form.
2026-10-01 09:54:11 +02:00
Nathan Vasse f7136473af 🔧(load-tests) declare the file size in the upload scenarios
Item creation now rejects a file without a reservation, so both session
scenarios would fail at the upload step. The size is read from the file the
scenario is about to send, keeping the generated traffic realistic.
2026-10-01 09:54:11 +02:00
Nathan Vasse aa635b6b23 ✅(e2e) cover upload reservations against real object storage
The reservation only holds if the signed Content-Length, MinIO and the quota
agree, which unit tests cannot show since they stub the storage. The e2e
stack now runs the local entitlements backend so admission is really
exercised, and a dedicated account keeps the quota assertions independent
from the items other specs leave behind.
2026-10-01 09:54:11 +02:00
Nathan Vasse bbf8a25ee4 ✨(frontend) send the file size when creating an upload
The API now requires a size reservation to hand back an upload policy, and
the browser already knows the length of the File it is about to send.
2026-10-01 09:54:11 +02:00
Nathan Vasse 0073a351cd ✨(backend) reserve the declared size before authorizing an upload
Clients now declare the size when creating a file item. The item is created
with that reservation and the entitlement decision is applied in the same
transaction, so a refusal rolls back the creation before any authorization
is returned.

The presigned URL signs Content-Length and finalization verifies the stored
object against the reservation. Storage usage stays a plain sum of size,
with no double counting once the upload ends, and a pending upload is
accounted for from the moment it is created.
2026-10-01 09:54:11 +02:00
Nicolas Clerc 57007843d1 ⬆️(mail) update vulnerable transitive dependencies
Re-resolve js-yaml, svgo and undici to their latest in-range versions
to close security advisories.
2026-09-30 21:35:05 +02:00
Nicolas Clerc 140fe359dc ⬆️(frontend) update vulnerable transitive dependencies
Re-resolve fast-uri and js-yaml to their latest in-range versions to
close security advisories.
2026-09-30 21:34:17 +02:00
Nicolas Clerc 761b478786 ⬆️(frontend) bump sharp to 0.35.5 for security fixes
sharp 0.35.4 ships a libheif with security fixes, 0.35.5 is
the latest patch release of the 0.35 line.
2026-09-30 19:03:35 +02:00
Nicolas Clerc fb1ccb3ba2 ⬆️(frontend) bump next to 15.5.27 for security fixes
next 15.5.24 fixes two critical security issues found in 15.5.21,
15.5.27 is the latest patch release of the 15.5 line.
2026-09-30 19:02:10 +02:00