Added
- ✨(backend) make the upload ACL configurable to support GCS based storages
- ✨(frontend) show the messages widget button on the homepage
- ✨(frontend) open the messages widget from the help menu
- ✨(backend) add an item batch share endpoint gated by ALLOW_SHARE_IMPORT_FILE
- ✨(frontend) share an item with contacts imported from a file
- ✨(backend) add a quota_excluded flag on items
- ✨(backend) apply per-audience attributes to external api items
- ✨(backend) add a grant_unlimited_storage command
Changed
- 🔧(docker) drop the unused pip upgrade and apk caches from the image
- ✨(backend) expose item existence in the malware detection admin
- ✨(backend) show human readable item size in the admin
- 🚚(global) move favorite items API endpoint to `/items/favorites/`
Fixed
- 🐛(docker) pin collabora image and adapt to its new runtime contract
- 🐛(backend) delete malware detection record when purging an item
- 🔒️(backend) reject unsafe filenames requested by WOPI renames
- 🔒️(backend) analyze file content written through WOPI
PutFile wrote the content straight to object storage, while malware
detection only ran on the upload endpoint. Editing a document was
therefore a way to store content that was never analysed.
The item stays READY during the analysis: a collaborator cannot open a
file that is not READY, so flipping the state would eject everyone from
the document on every save.
The requested name was decoded from UTF-7 and used as is, so a path
separator could reach the storage key and leave the item without an
extension. A second rename then granted it any extension, bypassing the
allowed list that only the upload path enforced.
Extensions are now compared case insensitively on both sides, so entries
declared with capitals are no longer dead.
Snyk Code flags the f-string path built from the request extension as
a path traversal. The DRF ChoiceField already restricts the value but
static analysis cannot follow it. Selecting the file name from a
static mapping makes the sink independent from user input, and the
serializer choices now derive from that single mapping.
The alpine 3.22 base of nginx-unprivileged carries 69 known CVEs
including a critical one in openssl (CVE-2026-31789). Renovate does
not manage Dockerfile images on this repo, so the bump is manual.
mjml v5 pulls commander@15 which requires node >= 22.12. Include the
yarn.lock and build scripts in the mail templates cache key so
dependency bumps regenerate the templates.
mjml v5 ignores mj-include by default, silently dropping the whole
mail head (Django load tag, title, fonts and styles) from the
generated templates. Opt back in.
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.
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.
Add a "Quarterly" keyword spanning a my-files, a shared and a starred
item to the search fixture, and assert the location filter narrows the
search results accordingly.
runTarget only ignored "make: *** No rule to make target", missing the
"make[N]:" prefix and non-English locales. Force LC_ALL=C and match the
message loosely.
Replace the search modal type/workspace/scope filters with the location,
file type, shared-with and modification date filters, matching the new
search design.
Move the explorer filters into a dedicated filters/ folder, one file per
filter. Make the modification date filter controlled so it resets when the
active filters are cleared from outside, and share the date-range merge
logic between the topbar and the search modal.
Cover the category, contact and modification date filters, and check
that an anonymous visitor on a public folder does not get the contact
filter, whose endpoints require authentication.
Add a "Modified" filter to the topbar with Today / Last 7 days /
Last 30 days / This year presets and a compact custom date range
picker, wired to the updated_at range filter.
Add a "Shared with" filter to the topbar, listing frequent contacts
and searching people by name, wired to the contacts endpoint and the
backend contact filter.
Replace the folder/file toggle in the topbar with a file type category
filter (text document, slides, spreadsheet, pdf, image, video, audio,
archive, other), wired to the backend category filter.
Add a single location query parameter to the search endpoint, exposing
the My files, Shared with me, Starred and Trashbin views. Trashbin
overrides the scope filter as it implies a deleted scope.
Centralize creator, favorite and owner filtering as ItemQuerySet
methods, so the list filters, the trashbin endpoint and the upcoming
search location filter share a single definition.
Add a contacts endpoint on the user viewset returning the people who
share items with the current user, ranked by the number of shared
items. It mirrors the contact filter perimeter (direct and team
access, live items only) so every suggestion yields a result.
Add an updated_at date range filter on the items list so results can
be narrowed to a modification period. Bounds match on the date part,
so a date-only bound covers the whole day rather than stopping at
midnight.
Add a contact filter on the items list returning the items in which
the given user is involved through a sharing access, in either
direction, so users can find files they share with a given person.
Add a `category` filter on the items list to narrow results by file
type (text document, slides, spreadsheet, pdf, image, video, audio,
archive, other). Categories are matched on the filename extension,
since several listed formats share ambiguous mime types.
The sharing filter needs a realistic contact picker with user names.
Switching from anonymous emails to a fixed list of named users gives
the demo database the full_name and short_name fields required for
that UI.
The --file-types flag creates one fixture per category (docx, xlsx,
pptx, pdf, image, video, zip, mp3, binary) so developers can populate
a local demo database covering all filter-relevant types, without
having to upload real files manually.
default_storage.open() downloads the whole object in memory before the
first read, so exporting folders holding large files can exhaust the
worker memory. Stream straight from the boto3 response body instead.
Conversion reads the source bytes, so it must not run before malware
analysis confirms the file is safe. convert_file now waits while the
source is analyzing and drops the placeholder if it never clears.
A file uploaded by a user must stay usable during malware analysis,
including conversion. Flag the convert ability while the source is being
analyzed so the frontend offers it.
The inline dummy backend reported files safe before the upload response
returned, so the analyzing upload state was never observable. Delay the
safe result off the request thread to expose it for frontend testing.
Marking the analyzing state as transient locked freshly uploaded items in
the UI while malware detection ran. The uploader must keep full access
during analysis, so split polling from the transient UI lock and surface
the analyzing label as a non-blocking badge. The 404 conversion toast is
now limited to converting placeholders.
Added
- ✨(backend) manage reconciliation requests for user accounts
- ✨(backend) add recursive folder export as ZIP archive
- ✨(frontend) add folder export action
- ✨(backend) background conversion of legacy Office files
- ✨(backend) allow grist file upload
- ✨(frontend) add CTA on public link for anonymous and authenticated users
Changed
- 🐛(backend) replace VersionId by Etag for WOPI
- 🐛(backend) sanitize slash in template-created filenames
Removed
- 🔥(backend) drop deprecated numchild columns from item
Without the secret the request reaches OnlyOffice unsigned and fails
with a cryptic error code -8. Validate in the synchronous path before
creating the placeholder so the API returns an error immediately, and
hide the convert ability from the API so the frontend does not offer
the action.
The backend now reads the secret directly from settings, like it
already does for timeouts.
When OnlyOffice has JWT enabled it adds its own Authorization header to
outgoing file-download requests. The WOPI access token in the query
param must take precedence so the opaque token lookup succeeds.
The OnlyOffice server rejects unsigned /converter requests with error
code -8 when JWT is enabled.
Add a dedicated WOPI_ONLYOFFICE_CONVERT_JWT_SECRET setting and fix the
JWT body token format to match the OnlyOffice conversion API: encode
parameters directly instead of wrapping them in {"payload": …}, and
send only {"token": "…"} in the request body.
Enable JWT on the development OnlyOffice container so the issue is
caught locally.
The .grist extension was already in FILE_EXTENSIONS_ALLOWED but
upload failed at MIME type validation because python-magic detects
grist files as application/vnd.sqlite3.
The conversion flow crosses modal state, API calls, and explorer refreshes.
Cover the expected user path with a mocked conversion ability and placeholder
response.
The same polling loop fits any transient upload state.
Rename the duplicating poller to a transient one driven by
TRANSIENT_UPLOAD_STATES so converting items are picked up without copying
the hook, and the explorer refreshes automatically when background
conversion finishes.
A converting file is visible but not ready for normal file actions.
Show the transient state consistently and prevent opening or acting on the item
until conversion completes.
Users should understand that opening a legacy file creates an editable copy.
Keep the modal focused on confirmation and let the folder show the ongoing
conversion state.
The frontend needs to treat conversion as item creation rather than job polling.
Return the placeholder item from the driver so UI state stays aligned with the
explorer list.
Uploaded filenames preserve extension case, while WOPI discovery stores
extension keys in lowercase.
Normalize the lookup so legacy files like REPORT.DOC follow the same conversion
path.
Starting a conversion should be quick for the API caller and continue in the
background.
Queue the placeholder-backed conversion task from the item endpoint.
The explorer needs to know when opening a legacy Office file should start
conversion instead of WOPI editing.
Expose the decision on item abilities so the frontend can keep the flow item-
based.
Users need immediate feedback in the current folder while conversion continues
in the background.
Create the target item up front and complete it once converted bytes are
available.
Converted files should appear in the folder immediately while Celery is still
working.
Add a dedicated transient state instead of overloading upload or duplication
states.
OnlyOffice must fetch the source bytes from Drive during server-side conversion.
Use short-lived WOPI access URLs so conversion does not depend on direct object
storage reachability.
Legacy Office formats need an explicit conversion policy before they can be
opened for editing.
Keep the mapping small and configuration-driven so the backend only converts
formats intentionally forced by the WOPI client.
Drive needs a server-side path to convert legacy Office files without opening an
interactive editor session.
Use OnlyOffice's conversion API as the first supported provider for the POC.
Issue #412 asks for a way to download an entire folder. Add a
GET /items/<id>/export/ action that streams a recursive ZIP archive of
the folder, preserving the directory hierarchy and skipping soft-deleted
or not-yet-uploaded descendants.
Introduce a new "export" entry in Item.get_abilities, true only for
folders the user can read. Will be the gate for the upcoming recursive
folder export endpoint.
The _deprecated_numchild and _deprecated_numchild_folder fields were
kept temporarily to preserve the underlying columns during the tree
model transition. The columns are no longer read or written, so the
fields and their columns can now be removed.