Operators may document how storage quotas work for their users. The
optional FRONTEND_STORAGE_GAUGE_INFORMATION_LINK setting is exposed
through the config API so the frontend gauge can link to that page.
Both actions grow the acting user's storage usage without going
through an upload: duplicating makes them creator of a new sized
copy, and moving a file to the root without a direct access
reassigns its creator. An over-quota user could use them to keep
taking ownership of storage. Moving also invalidates the previous
creator's usage cache since the reassignment only triggers the
post_save invalidation for the new creator.
Deployments without a DeployCenter service had no way to enforce
storage quotas. This backend computes each user's usage locally and
applies a configurable default limit, overridable per user in the
admin (0 meaning unlimited) and skippable for users created before a
cutoff so quotas can be rolled out to new users only. The quota is
soft: can_upload runs before the file size is known, so one upload
can overshoot before the next one is blocked.
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.
The quota gauge reads the storage usage from cache (directly for the
local backend, via DeployCenter's metrics for the remote one). Both
caches must be dropped as soon as a write changes a user's usage,
otherwise the gauge shows stale numbers until the timeout. A post_save
signal covers item saves, and the code paths doing bulk updates that
bypass signals (signup invitations, user reconciliation, hard delete
of a tree) invalidate explicitly for every impacted creator.
The frontend needs the storage usage and limit to render a quota
gauge. Entitlements backends can now return a quota (usage/limit,
locked when the organization quota is reached, or an explicit error
state) and the DeployCenter backend derives the can_upload reason
from the resolve level when the service does not provide one, so
the gauge and the upload errors stay consistent.
The upload gates returned a generic "permission_denied" code whatever
the entitlements backend's refusal reason. Forwarding the backend's
reason as the DRF error code lets the frontend map each rejection to
a specific, translatable message instead of a catch-all one.
Detection records whose item is gone can only be cleaned up manually.
Add an "item exists" column and filter to the admin so orphan records
can be spotted and deleted easily.
When an item was purged while its analysis was still pending or
processing, the detection record was left behind: the analysis kept
being relaunched and crashed on the missing file, leaving the record
stuck forever and holding a processing slot.
An empty AWS_S3_UPLOAD_ACL in an env file is easy to mistake for
an unset variable, and unset must keep meaning "private". The
explicit "default" value states the intent: let the bucket's
default object ACL apply. The frontend translates it where the
config is read, so the drivers keep sending exactly the ACL they
are given.
The upload ACL is now configurable and may be empty for storages that
do not support ACLs, so the PUT request must send exactly the ACL
signed in the upload policy. Read the value from the config endpoint
and pass it down to the S3 upload request.
GCS based storages like S3NS reject presigned PUT URLs signing the
x-amz-acl header: their S3 compatibility layer consumes x-amz-*
headers having an x-goog-* equivalent, so the signature check no
longer finds the header in the request. Objects are private by
default on S3 and GCS, so the ACL can simply be dropped by setting
AWS_S3_UPLOAD_ACL to an empty string. The setting is exposed in the
config endpoint so the frontend can send a header matching the
signature.
See https://documentation.s3ns.fr/storage/docs/aws-simple-migration
Rebase the backend image on python:3.13.14-alpine (Alpine 3.24) to
pull the patched openssl fixing CVE-2026-31789. Stays within the
pinned 3.13 runtime, so no dependency re-resolution is needed.
Next 15.5.18, Vite 6.4.3 and Turbo 2.9.14 patch several advisories:
Next.js server components denial of service (GHSA-26hh-7cqf-hhc6),
Vite server.fs.deny bypass (GHSA-fx2h-pf6j-xcff) and Turbo local
code execution on untrusted repositories (GHSA-3qcw-2rhx-2726).
Address the maintainability findings raised by the new SonarCloud
docker analyzer: drop the floating pip upgrade (nothing uses pip in
the image, the build relies on uv, and the base image already ships
a current pip), stop storing apk indexes in the image layers, quote
DRIVE_STATIC_ROOT and sort the core package list.
The collabora/code image published on 2026-07-18 (26.04.2.2.1) drops
the shell and the start script: our CMD-SHELL healthcheck could not
run at all (no /bin/sh) and extra_params was no longer read, so SSL
came back on and broke the plain-http WOPI discovery. Every e2e job
failed with "container drive-collabora-1 is unhealthy".
Pin the image and migrate to the new contract: coolwsd options go
through command arguments, and the health probe relies on the
built-in coolwsd --probe, overridden to target the http endpoint
since it defaults to https.
Added
- ✨(backend) allow converting a file while it is being analyzed
- ✨(frontend) add file type, contact and modification date topbar filters
- ✨(frontend) add location, file type, contact and date search filters
Fixed
- 🐛(nginx) let search engines see the noindex directive to drop public URLs
- 🐛(backend) find deleted root items when searching the trashbin
- 🐛(backend) exclude folders from file type search results
- 🐛(frontend) keep uploaded items usable while malware analysis runs
- 🐛(backend) stream export files from S3 without buffering
robots.txt blocked all crawling, so crawlers could never fetch the
pages and never saw the noindex meta tag: URLs discovered through
external links could still end up listed in search results.
Allow crawling in robots.txt and add a X-Robots-Tag noindex header on
frontend pages and media files so every response carries the directive.
Search rebuilds the visible set from the user's accesses, skipping those
whose item was soft deleted. A root item holds its own access, so
deleting it made it unreachable from any location, trashbin included.
Hard deleted items remain excluded, and the scope filter still keeps
deleted items out of regular search results.
The category filter keeps folders on explorer listings so navigation
stays possible. A search is not a navigation: folders have no file type
and only add noise to the results.
A reinstall picked up new radix-ui and typescript-eslint patch
releases within the ranges already declared, so the lockfile is
updated to keep local installs and CI resolving the same versions.
The reset entry moved out of the options list when the filters
switched to the ui-kit built-in reset, leaving a pointless wrapping
spread and a comment describing an option that no longer exists.
The ui-kit Filter emits an empty key when the active option is
deselected. Wrapping it in a structured value anyway made the filter
look active while carrying no usable bounds, so we now propagate
undefined to actually clear it.
The updated ui-kit renders the selected menu item indicator as an
SVG with the c__dropdown-menu-item__check class instead of the old
material-icons checked span, so the share e2e tests could no longer
find the icon and failed in CI.
On mobile the filter buttons overflowed the viewport with no way to
reach the last ones. The bar is now wrapped in a SmartScroller, like
in the search modal, and the mobile background moves to the container
so the surface color covers the whole scrollable area. The explorer
container also takes the full width under the tablet breakpoint.
Storing only the computed date bounds in the filters lost the selected
preset, so the filter could not restore its own state and the custom
range needed extra local state and top-down resets. The filters now
carry a frontend-only "modified" value (preset key or custom range)
and the drivers convert it to updated_at_* query params right before
each request. The custom calendar is hidden on mobile where it does
not have enough room to render comfortably.
The ui-kit now provides a dedicated user search filter with the reset
button, the empty state and the selected row rendering built in. The
bespoke SearchFilter wiring (manual reset entry, custom item renderer)
is no longer needed.
The Filter component now ships its own reset button, so the custom
reset option at the top of the type and location lists duplicated it.
Icons are also aligned with the design review: mini file icons and
small location icons.
The ui-kit renamed the accessible name of the user menu trigger from
"Open user menu" to "User menu". The tests use this button to detect
that the layout is fully loaded, so the selectors must follow.
Same cause as the empty doc baseline: the collabora update renders the
canvas 951px wide so the April screenshot fails on the size check. The
new baseline is the image rendered by the CI runner, taken from the
attempt matching the majority of retries (one retry showed a transient
scroll offset that the screenshot polling absorbs at comparison time).
Clearing the django session cookie can trigger a navigation while the
localStorage evaluation runs, which destroys the execution context and
fails the test on webkit. Retrying until the page settles removes the
race.
The CI pulls collabora/code:latest and a recent release changed the
canvas rendering: it is now 8px narrower and loses the page border, so
the screenshot comparison fails on the size check before even diffing.
The new baseline is the image produced by the CI runner itself.
The published ui-kit 0.24.0 renders the selected dropdown indicator as
a material icon span (.material-icons.checked). The class used before,
c__dropdown-menu-item__check, only exists in a not yet released ui-kit
version, so the locator matched nothing on the CI.
These tests never passed since their introduction. The modal title is
not rendered as a heading, so we target the dialog role instead. The
placeholder-injecting routes shadowed the convert ability mock and,
through route.continue(), sent the convert POST to the real backend
which denied it: they now re-inject the ability and use
route.fallback() so earlier mocks still apply. The confirming test
also mocks the placeholder GET, otherwise the poller sees a real 404
and removes the placeholder as a failed conversion.
Since ui-kit 0.24 the selected item indicator in dropdown menus is an
SVG instead of a material icon, so the item text no longer contains
"check" and the share link reach tests fail.
joserfc <1.6.8 accepts an empty HMAC key when verifying a JWT,
allowing token forgery if the secret is misconfigured (empty).
The dependency is pulled transitively via django-lasuite, which
does not enforce a minimum bound.
We verify the help menu shows the configured options, drops the contact
entry when no support email is set, and stays hidden when the config is
empty. The e2e env gets a help menu config so the menu is reachable.
When the backend provides FRONTEND_HELP_MENU_CONFIG, we render the
ui-kit HelpMenu in the left panel footer so users can reach the
documentation, legal pages and support. The menu is hidden when no
config is set to avoid an empty footer. We also hide the unused
cunningham toast container which would otherwise capture clicks on the
footer.
The frontend needs a configurable help menu pointing to documentation,
legal pages and a support contact. We expose FRONTEND_HELP_MENU_CONFIG
through the config endpoint so deployments can tune these links without
a frontend rebuild.
Committing the picked range on the calendar's onChange called setRange,
which rebuilt the Filter options memo and remounted the calendar. That
detached its OK button mid-interaction, so clicking OK raced against the
remount and timed out on firefox and webkit. We now stash the range in a
ref while picking and only commit it on OK, when the panel closes anyway.
The icons subpath import only exists in the ui-kit package.json
"exports" map. Jest's ts-jest transform forced classic "node"
module resolution, which ignores "exports" and failed to find
"@gouvfr-lasuite/ui-kit/icons". Switching to "bundler" matches
the app tsconfig so the unit tests compile again.
The new test picks a custom range through the calendar, asserts the
items are filtered to the selected window, checks the option label
reflects the bounds, and that resetting falls back to the default label.
The icons now live under the dedicated `@gouvfr-lasuite/ui-kit/icons`
entry point. Importing them from there keeps the icon imports aligned
with the package layout.
The filter row is wrapped in a SmartScroller so it overflows gracefully
when the filters do not fit. The global reset button is dropped since
each filter already exposes its own reset, making it redundant.