Skip to content

Modernize file storage and transport using Django Storage API - #3733

Open
cesarbenjamindotnet wants to merge 28 commits into
ohcnetwork:developfrom
xiidigital:django-storages
Open

Modernize file storage and transport using Django Storage API#3733
cesarbenjamindotnet wants to merge 28 commits into
ohcnetwork:developfrom
xiidigital:django-storages

Conversation

@cesarbenjamindotnet

@cesarbenjamindotnet cesarbenjamindotnet commented Aug 7, 2026

Copy link
Copy Markdown

This PR modernizes CARE's file storage implementation by adopting Django's Storage API and simplifying the HTTP file transport contract, according to #3731

The objective is to reduce provider-specific code while preserving local MinIO compatibility and enabling additional storage backends through configuration.

What changed

Storage

  • migrate object persistence to Django Storage API
  • introduce django-storages
  • preserve MinIO as the default local backend
  • support generic S3-compatible providers
  • support Google Cloud Storage through Django Storage
  • remove provider-specific persistence logic
  • centralize storage configuration

File transport

  • remove provider-generated signed upload URLs
  • remove provider-generated signed download URLs
  • replace base64 uploads with multipart uploads
  • route uploads through CARE
  • route downloads through CARE using Django FileResponse
  • remove provider-specific bucket URLs from the public API

Benefits

  • provider-neutral storage layer
  • frontend no longer depends on storage implementation
  • easier support for additional storage providers
  • native Django upload handling
  • lower upload overhead
  • simpler storage architecture
  • centralized authorization
  • improved maintainability

Local development

The existing Docker development workflow continues using MinIO through S3Storage.

No cloud provider is required for local development.

Breaking changes

This PR changes the file API contract.

Removed:

  • signed_url
  • read_signed_url
  • JSON/base64 upload transport

Added:

  • multipart uploads
  • CARE download endpoints (download_url)

The frontend must be updated accordingly.

Out of scope

This PR intentionally does not include:

  • asynchronous runtime changes;
  • Cloud Tasks;
  • Redis changes;
  • cache redesign;
  • distributed locking;
  • deployment infrastructure.

Those are planned as separate architectural phases.

Testing

The implementation has been validated with:

  • local MinIO integration
  • Django Storage tests
  • multipart upload tests
  • provider-neutral storage tests
  • full regression suite
  • end-to-end HTTP upload/download verification

Notes

The architectural work was intentionally split into independent phases.

This PR contains only the storage and file-transport modernization to keep review focused.

Merge Checklist

  • Tests added/fixed
  • Update docs in /docs
  • Linting Complete
  • Any other necessary step

Summary by CodeRabbit

  • New Features

    • Added multipart file uploads with improved validation and streaming downloads.
    • Added CARE-hosted download links for uploaded files and reports.
    • Added public, cacheable facility cover images and profile pictures.
    • Added configurable S3-compatible and Google Cloud Storage support.
    • Added provider-neutral storage without exposing external storage URLs.
  • Bug Fixes

    • Improved handling of missing files, cleanup, MIME types, filenames, and upload limits.
  • Documentation

    • Added architecture, storage, transport, deployment, operations, testing, and configuration guidance.

cesarbenjamindotnet and others added 21 commits August 5, 2026 15:32
Phase 0 inventory of CARE's runtime, verified against the code at
6a2976d. Documentation only; no application behavior changed.

Adds docs/xii/architecture/inventory/:
  storage-call-sites.md    19 object-storage call sites
  task-call-sites.md       8 task definitions, 4 async dispatch sites
  cache-and-redis.md       69 cache/Redis call sites, 3 hard couplings
  frontend-file-flow.md    file API contract and required changes
  runtime-and-deployment.md process/image/CI facts, baseline blockers
  plugin-impact.md         plugin loading; no plugins bundled
  unresolved-items.md      12 defects, 10 open questions, 8 unknowns

Corrections to 01-current-runtime.md:
  - remove stray ````markdown fence that made the frontmatter and
    sections 1-2 render as literal code
  - section 38: patient files do not use FILE_UPLOAD_REGION/KEY/SECRET;
    get_patient_bucket_config reads FACILITY_S3_* instead, leaving
    those three settings dead
  - section 40: note reports inherit the same FACILITY_S3_* credentials
  - section 26: add the three task definitions outside care/emr/tasks/
  - fix two wrong paths to 02-target-runtime.md
  - record verified_against_commit

Baseline commands could not run: Docker daemon down, make absent, no
Python 3.13 environment, Redis not running. Blockers documented in
runtime-and-deployment.md section 11.

All 84 cited paths and 46 cited symbols verified to exist.

Co-Authored-By: Claude <noreply@anthropic.com>
Record the Phase 0 green baseline for the current upstream-compatible
CARE runtime. Documentation only; no runtime, settings, model, migration
or dependency change.

runtime-and-deployment.md
- Replace the BLOCKED section 11 with the recorded baseline: environment,
  environment files, translated Makefile commands, build result, service
  health, startup sequence, migration and sync results, fixtures, tests.
- 312 migrations applied; makemigrations --check clean; 115 permissions,
  10 roles, 546 role-permissions, 30 valuesets; fixtures loaded.
- 1912 tests, 0 skipped, green on 2 of 3 --parallel --shuffle runs.

unresolved-items.md
- Mark Part E resolved and record the disposition of E1-E6.
- Withdraw E6: no root .env is required. docker/.local.env and
  docker/.prebuilt.env are tracked in git and have no .example variants.
- Add E7 (flaky test_password_request_rate_limiting under --parallel) and
  E8 (transient BuildKit pip cache wheel corruption).
- Resolve D8.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0001 / ES-01 (IS-01) milestone 1.

Adds django-storages 1.14.6 with the s3 and google extras so that
object persistence can move onto Django's Storage API. No application
code uses it yet.

Resolution notes:
- A full `pipenv lock` moved 59 unrelated packages, which ES-01 section 8
  forbids. Used `pipenv upgrade` instead, which merges only the resolved
  subtree into Pipfile.lock.
- All 44 direct [packages] pins are unchanged, including boto3 ==1.43.6
  (retained: SNS and the legacy signed-URL path still need it) and
  django ==6.0.
- dev-packages and docs categories are untouched.
- 12 packages added: django-storages plus the google-cloud-storage tree.
- 8 transitive packages moved as an unavoidable consequence of resolving
  the google extra: asgiref, botocore, certifi, cffi, charset-normalizer,
  cryptography, idna, s3transfer. None is a direct dependency.

django-storages 1.14.6 advertises Django support only through 5.1, so
compatibility with Django 6.0 was verified by rebuilding the image and
importing both S3Storage and GoogleCloudStorage.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0001 / ES-01 (IS-01) milestone 2. Configuration only; no application
code is routed through the new aliases yet.

Adds CARE_STORAGE_BACKEND (s3 default, gcs supported). An unsupported
value raises ImproperlyConfigured naming the supported values.

Defines the logical aliases patient, facility and report in STORAGES,
built by config/storage.py. staticfiles is untouched and stays on
WhiteNoise. `default` is deliberately still absent: Django 6.0 does not
merge STORAGES with its global defaults and nothing in CARE resolves that
alias, so adding it would change behaviour rather than preserve it.

Bucket names come from provider-neutral CARE_*_STORAGE_BUCKET variables,
each defaulting to the existing setting, so no local or deployed
configuration has to be rewritten. report shares the patient bucket, as
before, but is a separate alias so it can be repointed without touching
application code.

Behaviour change, per ES-01 section 26 (do not preserve a verified
settings bug): the patient and report aliases now read FILE_UPLOAD_REGION,
FILE_UPLOAD_KEY and FILE_UPLOAD_SECRET. get_patient_bucket_config and
get_report_bucket_config read the FACILITY_S3_* credentials instead,
leaving those three settings dead. Locally all of them resolve to the same
MinIO values, so there is no local change; a deployment that sets
FACILITY_S3_KEY to something other than BUCKET_KEY without also setting
FILE_UPLOAD_KEY will need to set it.

file_overwrite is set explicitly because CARE generates a unique
internal_name and the boto3 put_object being replaced overwrote
unconditionally; Django must not rename on collision. Verified against
MinIO: re-saving returns the same name and replaces the content.

BUCKET_HAS_FINE_ACL is honoured at the facility alias rather than per
object. The facility bucket's only writer is cover_image.py, so this is
equivalent. It is ignored under GCS, which requires uniform bucket-level
access.

Verified: all four aliases resolve; a MinIO save/exists/size/open/delete
round trip succeeds; open of a missing object raises FileNotFoundError;
GCS aliases construct with no credentials present; collectstatic and
manage.py check pass.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0001 / ES-01 (IS-01) milestone 3. Ordinary object persistence now
goes through Django Storage aliases. No provider client is constructed
for persistence anywhere in CARE.

file_manager.py replaces S3FilesManager with FilesManager, a transitional
wrapper that resolves a logical alias and delegates to Django Storage. It
imports no provider SDK and holds no provider branch. Retained rather
than removed because files_manager is a class attribute on FileUpload and
ReportUpload and is used by viewsets, tasks and report generation;
rewriting every caller was wider than IS-01 warrants.

get_storage_name() is the provider-neutral name helper. It preserves the
verified <file_type>/<internal_name> convention exactly, returns a
relative name with no bucket, URL or endpoint, and rejects traversal.
Both components are already server-controlled (FileTypeChoices, the
report-type registry, and a UUID internal_name), so the guard is defence
in depth.

Signed URLs move to the new legacy_signed_urls.py, isolated from ordinary
CRUD per ES-01 section 21 and listing its exact callers. It is the only
remaining provider SDK use for files and is S3-only; IS-02 removes it.

Migrated call sites:
- file_upload viewset put_object -> Storage.save (unchanged signature)
- report generation -> Storage.save, wrapping the already-materialised
  bytes in ContentFile
- cleanup task -> Storage.delete
- cover_image.py -> storages[facility] directly, keeping its own
  <folder>/<id>_<token>.<ext> key convention, and now streaming the
  uploaded file instead of passing raw bytes

delete_object no longer takes `quiet`. Django Storage delete is
idempotent on both backends, and so was the boto3 call it replaces: S3
delete_object does not raise NoSuchKey, so the old quiet=False branch
never fired.

report generation keeps an explicit content type. S3Storage prefers
content.content_type; GoogleCloudStorage derives it from the name, so the
extension in internal_name stays the portable signal. The only registered
generator is html/.html, which guesses identically.

test_file_upload_api no longer asserts botocore ClientError/NoSuchKey. It
now asserts the object does not exist and that opening it raises
FileNotFoundError -- provider-neutral and one assertion stronger.

care/emr/tasks/report_generation.py keeps autoretry_for=(ClientError,)
deliberately. It imports an exception class for retry configuration and
never instantiates boto3; under the default s3 profile django-storages
raises ClientError from inside Storage.save, so retry behaviour is
unchanged. It would not retry under gcs; recorded as a known gap rather
than changing Celery retry semantics, which ES-01 section 31 excludes.

Tests: 102 pass across file upload, facility, user and diagnostic report
suites, including real MinIO round trips.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0001 / ES-01 (IS-01) milestone 4. 50 new tests.

care/utils/tests/test_storage_config.py -- construction only, no provider
contact and no Google credentials:
- s3 is the default backend
- patient, facility and report resolve to S3Storage under s3
- staticfiles stays on WhiteNoise
- an invalid backend raises ImproperlyConfigured naming s3 and gcs
- report shares the patient bucket while staying a distinct alias
- alias names contain no provider name
- GCS variables are not required under s3, and S3 options are not leaked
  into a GCS alias
- GCS never uses object ACLs, and GoogleCloudStorage constructs with no
  credentials present
- the endpoint is omitted for AWS and included for S3-compatible

care/emr/tests/test_storage.py:
- the <file_type>/<internal_name> convention, including Unicode,
  multi-part extensions and unusual names, all preserved byte-for-byte
- names are relative: no bucket, URL or endpoint
- traversal, absolute paths and empty components raise
  SuspiciousFileOperation
- models are bound to logical aliases, cover images to facility
- delegation proven by substituting InMemoryStorage at the Django Storage
  boundary; no provider SDK is mocked
- MinIO integration on all three aliases: save, exists, size, open,
  delete, missing-object FileNotFoundError, idempotent delete, and
  overwrite keeping the name

Writing the delegation tests surfaced a real distinction worth recording:
overwrite-on-collision comes from the file_overwrite backend option, not
from Django Storage. InMemoryStorage renames instead. So overwrite is
asserted against the configured backends in the MinIO tests and the
option itself in the config tests, while the delegation test asserts only
that FilesManager reports back what the backend did.

Integration test names are UUID-based because the 16 parallel workers
share one MinIO.

Co-Authored-By: Claude <noreply@anthropic.com>
…rence

07-configuration-reference.md section 12.4 specifies GCS_PROJECT_ID. The
initial implementation used CARE_GCS_PROJECT_ID. The documentation is
authoritative, so the code is aligned to it rather than the reverse.

Optional either way: when unset, GoogleCloudStorage uses Application
Default Credentials and no project_id option is emitted.

Co-Authored-By: Claude <noreply@anthropic.com>
Documentation only.

inventory/storage-call-sites.md
- New section 11 marks the migration status of all 19 call sites:
  9 migrated_to_django_storage, 7 legacy_signed_url_only,
  2 temporary_wrapper, 1 removed, 1 not_storage_persistence, 0 blocked.
- Records the alias table, the object-name helper, the corrected
  credential defect from section 2.1 and its behaviour change, the two
  remaining provider SDK uses, and the whole-file reads that remain.
- Sections 1-8 are left as the Phase 0 snapshot; section 11 is current
  where they disagree.

inventory/frontend-file-flow.md
- New section 11: no route, request field or response field changed.
- Maps the file and line references IS-01 relocated.
- Records which persistence now uses Django Storage, and which signed
  upload, signed download and base64 flows remain for IS-02.
- Marks D3 partly done and C3 resolved at the settings layer; the rest of
  the section 9 change list is unchanged.
- Records that legacy_signed_urls is S3-only, so IS-02 is a prerequisite
  for a real GCS deployment.

inventory/unresolved-items.md
- New Part B2 with S1 (GCS cannot serve files end to end), S2 (report
  retry does not fire under GCS), S3 (overwrite safety depends on a
  backend option) and S4 (items unchanged by IS-01).
- B10 resolved: delete_objects removed.
- B1 partly resolved: the aliases read the correct credentials; the two
  csp/config.py resolvers still carry the defect but are now reached only
  by the legacy signed-URL path.
- E7 scope corrected. It is not one flaky test but a defect class: six
  tests across rate limiting and favorites, all caused by a single Redis
  cache shared by 16 parallel workers while three modules call
  cache.clear() in setUp. Measured: full suite serial 1962/1962 OK; full
  suite parallel 1 of 6 green; the three cache-touching modules alone
  fail 4 of 4 in parallel with no storage code involved. Includes the
  recommended per-worker KEY_PREFIX fix.

02-target-runtime.md section 11
- States the four required claims explicitly: Django Storage API is the
  architecture; MinIO through S3Storage is the default local profile;
  generic S3-compatible storage remains supported; GCS is the initial GCP
  profile, not the only supported provider.
- Adds the post-IS-01 status of the three SHALL NOT clauses, naming the
  two transitional exceptions.

07-configuration-reference.md
- 11.2: records CARE_STORAGE_BACKEND as implemented, s3 default, and that
  filesystem is not a supported value per ES-01 section 9.
- 12.8: corrects the suggestion that GCS_FILE_OVERWRITE=false may suit
  unique immutable names. false does not reject duplicates, it silently
  renames, which is exactly the database-reference hazard the section
  warns about. Overwrite is enabled on every alias, with the tests that
  demonstrate both behaviours cited.

Co-Authored-By: Claude <noreply@anthropic.com>
… Django

ADR-0001 completion. Object transport is now entirely mediated by CARE:
no application code generates a storage-provider URL, and no client ever
receives one.

Removed:
- care/emr/utils/legacy_signed_urls.py (presigned PUT and GET generation)
- care/utils/csp/ entirely: BucketType, CSProvider, ClientConfig and
  get_client_config existed only to resolve provider credentials and
  external endpoints for those signed URLs
- the signed_url response field on FileUploadRetrieveSpec and
  ReportUploadRetrieveSpec, and the _just_created write branch
- FACILITY_CDN, BUCKET_HAS_FINE_ACL, BUCKET_EXTERNAL_ENDPOINT,
  FILE_UPLOAD_BUCKET_EXTERNAL_ENDPOINT and
  FACILITY_S3_BUCKET_EXTERNAL_ENDPOINT, which had no consumer left
- the public-read ACL on the facility alias; every bucket is now private

Added care/emr/utils/file_download.py: a provider-neutral FileResponse
helper that streams through Django Storage. It carries the
inline-vs-attachment decision the presigned ResponseContentDisposition
used to make, so browser handling of patient documents is unchanged.

Download routes:
- GET /api/v1/files/{external_id}/download/ -- authorized by the existing
  get_queryset file_authorizer, which runs for every detail action
- GET /api/v1/template_reports/{external_id}/download/ -- authorizes
  explicitly, because ReportUploadViewSet.get_queryset only guards list
- GET /api/v1/assets/facility/{external_id}/cover_image/
- GET /api/v1/assets/user/{username}/profile_picture/

read_signed_url is replaced by download_url, a CARE route.

The two asset routes are unauthenticated, and deliberately so. Cover
images and avatars are already world-readable: AllFacilityViewSet and
FacilitySchedulableUsersViewSet are anonymous and expose them. Who can
see an image is unchanged; what changes is that CARE serves the bytes,
so the bucket becomes private. They are separate views because
FacilityViewSet and UserViewSet filter their querysets by request.user
and cannot serve an anonymous request.

Facility.read_cover_image_url and User.read_profile_picture_url now
reverse to those routes instead of concatenating a bucket URL, which also
removes the bucket-name divergence between persistence and URL building
that the review found.

BUCKET_PROVIDER survives as the credential-source switch, now compared
against a literal in config/storage.py rather than the deleted enum.

Tests: the presigned PUT round trip in test_upload_patient_file is
replaced by the Django upload endpoint plus a download through CARE,
asserting the route is a CARE URL and that Content-Type and
Content-Disposition are preserved. test_cleanup_incomplete_file_uploads
writes its object through Django Storage. The
FILE_UPLOAD_BUCKET_EXTERNAL_ENDPOINT override, which existed only for
signed URLs, is gone. 12 file API tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
plugin-impact.md records that S3FilesManager is importable from
care.emr.utils.file_manager and that any plugin may use it. Renaming it
to FilesManager in the previous commit was therefore a silent breaking
change for external plugins. The name is restored as a thin subclass.

Despite the name it is not S3-specific: it is FilesManager, delegating to
whichever provider the alias is configured for. It emits a
DeprecationWarning, accepts the historical positional argument that used
to be a BucketType, and maps PATIENT/FACILITY/REPORT onto the logical
aliases. An unrecognised value raises rather than silently writing to the
wrong bucket.

It deliberately exposes no signed_url or read_signed_url method. Those
were removed with the provider-specific transport, and a plugin must not
be able to mint a bucket URL that bypasses CARE.

Also updates the module docstring, which still pointed at the deleted
legacy_signed_urls module.

Co-Authored-By: Claude <noreply@anthropic.com>
New care/emr/tests/test_storage_transport.py (25 tests) asserts the
property the architecture depends on rather than the mechanics:

- the base64 upload endpoint still works and returns no provider URL
- no response offers signed_url or read_signed_url
- download_url is a CARE route, matched against reverse()
- detail and list responses are scanned for provider markers (minio,
  amazonaws, storage.googleapis, X-Amz-Signature, GoogleAccessId, :9100)
- downloading through Django returns the stored bytes
- download enforces authorization; a different user gets 403
- a missing object yields 404, not a provider error
- facility cover image and user avatar URLs are CARE routes that serve
  the stored bytes
- the cover-image route stays anonymous, matching the world-readable
  bucket objects it replaces, and 404s when no image is set
- every alias round-trips through Django Storage
- no manager exposes a signed-URL helper

Plugin compatibility: S3FilesManager imports, warns, delegates to Django
Storage, maps each legacy bucket name, rejects unknown aliases, and
exposes no signed-URL method.

Updated test_storage_config for the removed default_acl parameter and
added a test that no configured alias can be public.

68 storage tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0001
- Implementation checklist: IS-01 complete, legacy storage removed,
  legacy signed URL flows removed.
- Records what IS-01 delivered and that only the base64 upload transport
  remains for IS-02.

storage-call-sites.md
- Final per-call-site status: 11 migrated, 2 temporary wrapper, 6
  removed. Nothing is legacy_signed_url_only; nothing is blocked.
- New 11.1a: the four CARE routes that now mediate transport, their
  authorization, and why the two asset routes are anonymous.
- New 11.3a: one source of truth per logical bucket. Records that this
  closes a defect introduced earlier in IS-01, where setting
  CARE_PATIENT_STORAGE_BUCKET moved persistence but not the signed URLs,
  so uploads and downloads silently addressed different buckets on the
  default s3 profile.
- 11.5 rewritten: no storage module imports a provider SDK. Lists the
  deleted settings and the csp package.
- New 11.8: the two remaining compatibility layers.

frontend-file-flow.md
- 11.3 rewritten: signed upload, signed download and unsigned bucket URLs
  are all gone; base64 upload is all that remains for IS-02.
- 11.4: 13 of the 18 section 9 change items are now done.
- 11.5: contract impact as delivered, including that the four URL fields
  are now relative paths where they used to be absolute.
- 11.6: GCS is no longer blocked on transport.

plugin-impact.md
- Corrects the row claiming a plugin can mint presigned URLs through
  S3FilesManager.
- New section 9: deprecation notice with a before/after table of every
  behavioural change, the DeprecationWarning, the migration target, and
  the fact that the no-signed-URL guarantee is CARE's rather than
  enforced against plugin code.

07-configuration-reference.md
- Section 13 corrected. It specified S3_ACCESS_KEY, S3_SECRET_KEY,
  S3_ENDPOINT_URL, S3_REGION_NAME, S3_ADDRESSING_STYLE and
  S3_SIGNATURE_VERSION; none exists. Replaced with the actual per-alias
  credential and endpoint variables, BUCKET_PROVIDER as the credential
  source, and the bucket variables as the single point of resolution.
- Records that addressing_style and signature_version are not
  configurable, and what that rules out.

02-target-runtime.md
- Section 11 status: all three SHALL NOT clauses now hold for persistence
  and transport. Records that transport is entirely CARE-mediated and
  that csp/ is deleted.

unresolved-items.md
- S1 resolved: the GCS profile serves files end to end; IS-02 is no
  longer a prerequisite for a GCS deployment.
- S2 is now the only item blocking that profile.
- B9 largely defused: no client can write to the bucket, so an
  unverified mark_upload_completed is bookkeeping rather than an
  unverified external write.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0002 / ES-02. The last remaining transport issue: the Django-mediated
upload still carried complete file contents as base64 inside JSON.

POST /api/v1/files/upload-file/ now accepts multipart/form-data. The
binary part is `file`; the base64 `file_data` field is removed with no
fallback, per ES-02 section 23 (greenfield, no production client).

FileUploadMultipartSerializer declares the transport contract and the
binary field. It deliberately does not restate the domain rules:
FileUploadCreateSpec remains the authoritative validator for file type,
category and filename, so there is no second source of truth.
`original_name` becomes optional and defaults to the uploaded part's
filename, which multipart supplies and base64 could not.

The file is never materialised:
- Django's upload handlers decide InMemoryUploadedFile vs
  TemporaryUploadedFile before the view runs;
- size is read from UploadedFile.size, not by reading content;
- MIME sniffing reads only the leading 2048 bytes and seeks back, as
  before, so a browser-declared Content-Type is still not trusted;
- the UploadedFile is handed straight to Storage.save() with no
  ContentFile round trip.

Upload limits are now explicit rather than implicit, per ADR-0002. The
added FILE_UPLOAD_MAX_MEMORY_SIZE and DATA_UPLOAD_MAX_MEMORY_SIZE are set
to Django's own defaults, so behaviour is unchanged: anything over 2.5 MB
is temp-file backed, and MAX_FILE_UPLOAD_SIZE still caps the total at
5 MB. DATA_UPLOAD_MAX_MEMORY_SIZE does not bound the file, because
multipart file parts are exempt from it.

Worth noting: base64 inflated a 5 MB file to roughly 6.7 MB of JSON body,
which exceeds Django's 2.5 MB DATA_UPLOAD_MAX_MEMORY_SIZE. The old
endpoint could not actually accept a file near its own documented limit
over JSON. Multipart removes that.

Persistence, object naming, alias selection and authorization are
untouched: still Storage.save() through the patient alias, still
get_storage_name, still authorize_create -> file_authorizer. The DB row
is still written first inside the transaction so a storage failure rolls
it back.

Existing tests are updated to send multipart. test_upload_patient_file
also stops conflating two rows: it previously created a row via
POST /files/, uploaded a different row, then marked the first complete.

30 upload and transport tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
39 focused tests in care/emr/tests/test_file_upload_multipart.py,
covering ES-02 sections 31, 33 and 34.

Successful upload: record contents, patient alias, the ES-01 object name
unchanged, stored bytes match, download works, response carries no
provider marker and no bucket or endpoint key.

Rejection: missing file, missing metadata, oversized file (and that an
oversized file persists nothing), and that the old base64 `file_data`
body no longer works even by accident.

Validation: the declared part Content-Type is not trusted. A shell script
sent as image/jpeg is rejected on sniffed content; a real JPEG sent as
application/x-sh is accepted and recorded as image/jpeg. Blocked,
uppercase and double extensions behave as before, as do unknown file
type and category.

Authorization: unauthenticated and unauthorized uploads are rejected and
persist nothing.

Upload handlers: with a lowered FILE_UPLOAD_MAX_MEMORY_SIZE, the view is
verified to receive an InMemoryUploadedFile below the threshold and a
TemporaryUploadedFile above it, and the temp-file path round-trips.

Failure consistency: a storage failure reports failure and leaves no row;
a database failure after the object is written also rolls the row back.
The surviving gap is the orphan object, which is the pre-existing B8.

Provider neutrality: the whole upload and download flow is exercised
against InMemoryStorage substituted at the Django Storage boundary, which
would break on any surviving S3 assumption. A further test parses the AST
of the three transport modules to assert none imports a provider SDK and
none reads CARE_STORAGE_BACKEND. It inspects the AST rather than the text
because those modules legitimately mention boto3 and the backend classes
in prose describing what they replaced.

Schema: the request is multipart/form-data only, `file` is string/binary,
`file_data` is absent, and `original_name` is not required.

Also adds BinaryFileField. drf-spectacular renders a plain FileField as
format: uri, because DRF serialises it to a URL on output; in a multipart
request body it is raw bytes. Annotating the field is preferable to
enabling COMPONENT_SPLIT_REQUEST, which would reshape every schema in the
project.

Co-Authored-By: Claude <noreply@anthropic.com>
ADR-0002
- Implementation checklist complete: base64 removed, multipart
  implemented, upload handlers verified, size limits configured, schema
  implemented, authorization tested, MinIO round trip verified,
  provider-neutral behaviour verified, ES-02 complete.
- Notes that CARE never materialises the file, that range requests remain
  undesigned despite FileResponse advertising them, and that
  POST /files/ plus mark_upload_completed are now vestigial.

frontend-file-flow.md
- 11.3: base64 upload marked removed.
- New section 12: flow status table (signed upload, signed download,
  unsigned bucket URLs, base64 all removed; multipart upload and
  server-mediated download implemented), the multipart request contract
  with each part, the FormData migration snippet for the external
  frontend repository, and the memory/size table.
- Records that base64 inflated a 5 MB file past
  DATA_UPLOAD_MAX_MEMORY_SIZE, so the old endpoint could not accept a
  file at its own documented limit over JSON.
- Records what ES-02 did not change: cover images and avatars already
  used ordinary multipart and never touched the base64 endpoint;
  authorization, naming, alias selection and persistence are untouched.

storage-call-sites.md
- Call site 12 now covers transport as well as persistence.
- The base64 whole-file read is marked gone.
- Removes the note describing base64 as a remaining transport.

07-configuration-reference.md
- Section 15 corrected. It specified CARE_MAX_UPLOAD_SIZE,
  CARE_ALLOWED_UPLOAD_MIME_TYPES, CARE_ALLOWED_UPLOAD_EXTENSIONS and
  CARE_BLOCKED_UPLOAD_EXTENSIONS; none exists. Replaced with the real
  MAX_FILE_UPLOAD_SIZE (megabytes, not bytes), the two explicit Django
  memory settings, ALLOWED_MIME_TYPES with the note that the value is
  sniffed rather than taken from the request, and a pointer to
  FileNameValidator for extension policy.
- 16.1 corrected: inline behaviour comes from SAFE_INLINE_FORMATS in
  code, not a CARE_INLINE_MIME_TYPES setting.

Co-Authored-By: Claude <noreply@anthropic.com>
@cesarbenjamindotnet
cesarbenjamindotnet requested a review from a team as a code owner August 7, 2026 07:14
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 43c8286e-d192-412a-b03f-b40b5ff3bbf9

📥 Commits

Reviewing files that changed from the base of the PR and between 0bde688 and 699b936.

📒 Files selected for processing (10)
  • care/emr/tests/test_file_upload_api.py
  • care/emr/tests/test_file_upload_multipart.py
  • care/emr/tests/test_storage_transport.py
  • care/utils/tests/base.py
  • docs/xii/adr/ADR-0001-django-storage.md
  • docs/xii/architecture/inventory/cache-and-redis.md
  • docs/xii/architecture/inventory/frontend-file-flow.md
  • docs/xii/architecture/inventory/storage-call-sites.md
  • docs/xii/architecture/inventory/task-call-sites.md
  • docs/xii/architecture/inventory/unresolved-items.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • care/emr/tests/test_file_upload_api.py
  • docs/xii/architecture/inventory/task-call-sites.md
  • docs/xii/architecture/inventory/storage-call-sites.md
  • care/emr/tests/test_file_upload_multipart.py
  • docs/xii/architecture/inventory/cache-and-redis.md
  • docs/xii/architecture/inventory/unresolved-items.md
  • docs/xii/architecture/inventory/frontend-file-flow.md

📝 Walkthrough

Walkthrough

The pull request moves CARE file persistence to Django Storage aliases, replaces base64 uploads with multipart uploads, adds CARE-mediated streaming downloads, and serves public image assets through CARE routes. It also adds backend configuration, compatibility coverage, tests, and architecture documentation.

Changes

Storage and file transport modernization

Layer / File(s) Summary
Django Storage configuration and persistence
Pipfile, care/emr/utils/file_manager.py, config/storage.py, config/settings/*, care/emr/models/*, care/utils/file_uploads/cover_image.py
Storage uses logical patient, facility, and report aliases. S3 and GCS backends are configurable. A deprecated S3 compatibility wrapper remains.
Multipart uploads and downloads
care/emr/api/viewsets/*, care/emr/utils/file_download.py, care/emr/resources/*, care/emr/reports/*
Uploads use multipart files. Downloads stream through CARE routes. Provider-specific URLs and base64 payloads are removed.
Public image routes
care/emr/api/viewsets/file_assets.py, config/api_router.py, care/facility/models/facility.py, care/users/models.py
Facility cover images and user profile pictures use CARE asset routes backed by Django Storage.
Validation and compatibility tests
care/emr/tests/*, care/utils/tests/*
Tests cover storage behavior, multipart validation, authorization, streaming, backend substitution, provider-neutral responses, public assets, and compatibility behavior.
Architecture and migration documentation
docs/xii/adr/*, docs/xii/architecture/*, docs/xii/implementation/*, docs/xii/prompts/*
ADRs, inventories, implementation specifications, and deployment documentation describe the storage, transport, runtime, testing, and synchronization changes.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Possibly related issues

Possibly related PRs

  • ohcnetwork/care#3732 — Covers the same storage, file transport, upload, download, and configuration changes.

Suggested reviewers: vigneshhari

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: modernization of file storage and transport through Django Storage API.
Description check ✅ Passed The description covers proposed changes, issue context, architecture impact, testing, breaking changes, scope, documentation, and checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (15)
docs/xii/implementation/ES-02-file-transport-modernization.md-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the outer Markdown fence.

The fence at Line 1 encloses the specification as code. Nested fences then render incorrectly. This makes the implementation document unnecessarily difficult to use.

Proposed fix
-```markdown
 # ES-02: File Transport Modernization
 ...
-```

Also applies to: 1180-1180

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/implementation/ES-02-file-transport-modernization.md` at line 1,
Remove the outer Markdown code fence surrounding the implementation
specification in “ES-02: File Transport Modernization,” including its opening
and closing delimiters, while preserving the document content and any intended
nested fences.

Source: Linters/SAST tools

docs/xii/implementation/ES-02-file-transport-modernization.md-95-109 (1)

95-109: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the current docs/xii/ document paths.

The required-document list and ADR update target an obsolete docs/ hierarchy. The current specifications and inventories are under docs/xii/. An implementer cannot reliably complete the required reading or update the ADR checklist otherwise.

  • docs/xii/implementation/ES-02-file-transport-modernization.md#L95-L109: Replace the docs/architecture/, docs/adr/, and docs/specifications/ references with the current docs/xii/architecture/, docs/xii/adr/, and docs/xii/implementation/ paths.
  • docs/xii/implementation/ES-02-file-transport-modernization.md#L1044-L1078: Update the ADR checklist path to docs/xii/adr/ADR-0002-file-transport.md.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/implementation/ES-02-file-transport-modernization.md` around lines
95 - 109, Update the required-document list in
docs/xii/implementation/ES-02-file-transport-modernization.md lines 95-109 to
use the current docs/xii/architecture/, docs/xii/adr/, and
docs/xii/implementation/ paths. Also update the ADR checklist in lines 1044-1078
to reference docs/xii/adr/ADR-0002-file-transport.md.
care/emr/tests/test_file_upload_multipart.py-299-305 (1)

299-305: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test does not test what its name and comment claim.

The name says "incomplete rows remain cleanable" and the comment says the path "still sets upload_completed=False". The assertions then require upload_completed to be True and the object to exist. A successful multipart upload is complete, so nothing here is cleanable. Either rename the test to describe the completed-row invariant, or build a genuinely incomplete row and run cleanup_incomplete_file_uploads against it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/tests/test_file_upload_multipart.py` around lines 299 - 305, Correct
test_incomplete_rows_remain_cleanable so its setup and assertions match its
intent: either rename it to describe the completed multipart-upload invariant
while preserving the current successful-upload assertions, or create an
incomplete upload row with upload_completed=False and exercise
cleanup_incomplete_file_uploads to verify it remains cleanable. Do not leave the
current misleading name and comment paired with assertions for a completed
upload.
care/emr/utils/file_manager.py-149-153 (1)

149-153: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Annotate _LEGACY_ALIASES with ClassVar.

Ruff reports RUF012 for this mutable class-level dict, even though it is used only as shared read-only lookup state. Add typing.ClassVar and run ruff check --fix . / ruff format ..

Proposed fix
 import logging
 import warnings
+from typing import ClassVar

-    _LEGACY_ALIASES = {
+    _LEGACY_ALIASES: ClassVar[dict[str, str]] = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/utils/file_manager.py` around lines 149 - 153, Annotate the mutable
class-level dictionary `_LEGACY_ALIASES` with `typing.ClassVar` to satisfy Ruff
RUF012 while preserving its shared read-only lookup behavior. Add the required
typing import, then run `ruff check --fix .` and `ruff format .`.

Sources: Coding guidelines, Linters/SAST tools

docs/xii/adr/ADR-0002-file-transport.md-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the outer Markdown fence.

The file starts with a markdown fence at Line 1 and closes it at Line 515. This wraps the entire ADR in a code block, so headings, lists, and inner examples render as literal code. Remove both wrapper lines and keep fences only around the examples.

Also applies to: 515-515

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/adr/ADR-0002-file-transport.md` at line 1, Remove the outer Markdown
code fences wrapping the entire ADR, including the opening fence at the document
start and its matching closing fence at the end. Preserve all inner fences that
intentionally delimit examples so headings and lists render as Markdown.

Source: Linters/SAST tools

care/emr/resources/file_upload/spec.py-107-115 (1)

107-115: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not advertise a download URL for incomplete uploads.

download_url is assigned for every FileUploadRetrieveSpec, but detail access still permits rows with upload_completed=False. The new multipart flow writes the storage object before setting that flag, so an incomplete row can expose a URL that cannot resolve to an object. Return None until the upload is complete, and reject incomplete objects in the download action.

Proposed serialization fix
-        mapping["download_url"] = file_download_url(obj)
+        mapping["download_url"] = (
+            file_download_url(obj) if obj.upload_completed else None
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/resources/file_upload/spec.py` around lines 107 - 115, Update
FileUploadRetrieveSpec.perform_extra_serialization to assign download_url only
when obj.upload_completed is true, leaving it None for incomplete uploads. Also
update the download action that serves file uploads to reject objects with
upload_completed=false before resolving or returning the storage URL.
docs/xii/adr/ADR-0001-django-storage.md-81-83 (1)

81-83: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to the fenced code blocks.

markdownlint reports MD040 for these fences. Use text for the storage path and env or text for the configuration values.

Also applies to: 135-137, 148-150

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/adr/ADR-0001-django-storage.md` around lines 81 - 83, Add language
identifiers to the fenced code blocks in ADR-0001, including the blocks around
the S3Storage path and the configuration values at the referenced locations. Use
text for the storage path and env or text for configuration-value blocks so all
fences satisfy markdownlint MD040.

Source: Linters/SAST tools

docs/xii/architecture/02-target-runtime.md-17-34 (1)

17-34: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the cross-document paths before merge.

These references use docs/gcp/... or docs/xii/gcp/..., while the reviewed documents use docs/xii/architecture/.... The current links will not resolve to the documented files.

  • docs/xii/architecture/02-target-runtime.md#L17-L34: update the current-runtime and migration-plan paths.
  • docs/xii/architecture/00-scope-and-goals.md#L845-L851: update the next-document path.
  • docs/xii/architecture/02-target-runtime.md#L1874-L1880: update the next-document path.
  • docs/xii/architecture/03-migration-plan.md#L9-L13: update all depends_on paths.
  • docs/xii/architecture/03-migration-plan.md#L2008-L2014: update the testing-document path.
  • docs/xii/architecture/04-testing.md#L9-L13: update all depends_on paths.
  • docs/xii/architecture/04-testing.md#L1960-L1966: update the upstream-sync document path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/02-target-runtime.md` around lines 17 - 34, Correct the
broken cross-document references across
docs/xii/architecture/02-target-runtime.md lines 17-34 and 1874-1880,
docs/xii/architecture/00-scope-and-goals.md lines 845-851,
docs/xii/architecture/03-migration-plan.md lines 9-13 and 2008-2014, and
docs/xii/architecture/04-testing.md lines 9-13 and 1960-1966. Replace the
outdated docs/gcp or docs/xii/gcp paths with the corresponding
docs/xii/architecture paths, preserving each link’s intended target and
relationship.
docs/xii/architecture/04-testing.md-1811-1835 (1)

1811-1835: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a test-only task backend allowlist.

The runtime documentation says CARE_TASK_BACKEND supports cloud_tasks, celery, and postgres, but the quick unit-test example sets fake. Use CARE_TASK_BACKEND=fake only as an explicit test-only value and document that production settings reject it, so this example does not conflict with the documented backend contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/04-testing.md` around lines 1811 - 1835, Update the
“Test Configuration” section to explicitly define fake as a test-only
CARE_TASK_BACKEND value, clarify that production configuration rejects fake, and
preserve the existing real-backend integration-test guidance so the unit-test
example does not conflict with the runtime backend contract.
docs/xii/architecture/04-testing.md-1367-1376 (1)

1367-1376: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the Terraform checks cover the whole layout.

The snippet claims coverage for all modules and environments, while these commands target only the current directory. For the layout in docs/xii/architecture/03-migration-plan.md, include -recursive, and run terraform validate through deploy/gcp/terraform root and each environment/module that requires initialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/04-testing.md` around lines 1367 - 1376, Update the
“23.2 Formatting and validation” instructions to use recursive Terraform
formatting across the full layout, and document validation from
deploy/gcp/terraform plus each environment/module requiring initialization.
Align the commands with the directory structure described in
03-migration-plan.md so the stated all-modules and all-environments coverage is
accurate.
docs/xii/architecture/05-upstream-sync.md-10-15 (1)

10-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align all documentation references with the repository’s actual documentation root.

The file mixes docs/gcp/... references with the supplied architecture tree under docs/xii/.... Unless docs/gcp is an intentional generated mirror, these paths break dependency tracking, search commands, conflict-register discovery, and document navigation.

  • docs/xii/architecture/05-upstream-sync.md#L10-L15: replace the depends_on paths with the corresponding docs/xii/architecture/... paths.
  • docs/xii/architecture/05-upstream-sync.md#L704-L708: update the search examples to use the approved docs/xii root.
  • docs/xii/architecture/05-upstream-sync.md#L884-L888: use the approved conflict-register path under docs/xii.
  • docs/xii/architecture/05-upstream-sync.md#L1162-L1168: replace the next-document path with docs/xii/architecture/06-operations.md.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/05-upstream-sync.md` around lines 10 - 15, Align every
documentation reference in docs/xii/architecture/05-upstream-sync.md with the
approved docs/xii root: update the depends_on entries at lines 10-15 to their
corresponding docs/xii/architecture paths, revise the search examples at lines
704-708, use the docs/xii conflict-register path at lines 884-888, and set the
next-document reference at lines 1162-1168 to
docs/xii/architecture/06-operations.md.
docs/xii/architecture/06-operations.md-9-15 (1)

9-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the repository’s current documentation tree.

Both documents are under docs/xii/architecture/, but their dependency and next-document references use docs/gcp/. Update every affected reference to the actual docs/xii/architecture/ path.

  • docs/xii/architecture/06-operations.md#L9-L15: Update the dependency paths.
  • docs/xii/architecture/06-operations.md#L2527-L2533: Update the next-document path.
  • docs/xii/architecture/07-configuration-reference.md#L9-L16: Update the dependency paths.
  • docs/xii/architecture/07-configuration-reference.md#L2842-L2848: Update the next-document path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/06-operations.md` around lines 9 - 15, Update all
dependency and next-document references in
docs/xii/architecture/06-operations.md at lines 9-15 and 2527-2533, and
docs/xii/architecture/07-configuration-reference.md at lines 9-16 and 2842-2848,
replacing the incorrect docs/gcp/ paths with the corresponding
docs/xii/architecture/ paths.
docs/xii/architecture/inventory/storage-call-sites.md-368-369 (1)

368-369: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the migration totals or define their scope.

For numbered sites 1–19, the table contains 9 migrated sites, 2 temporary wrappers, and 8 removed sites. If the two unnumbered facility/user URL builders are included, the totals become 11 migrated, 2 temporary, and 8 removed across 21 sites. The current 11 migrated, 2 temporary, 6 removed does not match the table.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/inventory/storage-call-sites.md` around lines 368 -
369, Update the migration totals in the summary to match the documented
call-site counts: use 9 migrated, 2 temporary wrappers, and 8 removed for
numbered sites 1–19, or explicitly define a broader scope and use its
corresponding 11 migrated, 2 temporary, and 8 removed totals including the two
unnumbered builders.
docs/xii/architecture/inventory/cache-and-redis.md-474-474 (1)

474-474: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to all flagged Markdown fences.

Apply text, bash, dockerfile, http, or the appropriate language at each listed site. This removes the repeated MD040 lint failures.

  • docs/xii/architecture/inventory/cache-and-redis.md#L474-L474: mark the per-file breakdown as text.
  • docs/xii/architecture/inventory/frontend-file-flow.md#L94-L94: mark the request-field block.
  • docs/xii/architecture/inventory/frontend-file-flow.md#L517-L517: mark the HTTP request block.
  • docs/xii/architecture/inventory/plugin-impact.md#L256-L256: mark the download route block.
  • docs/xii/architecture/inventory/runtime-and-deployment.md#L43-L43, #L73-L73, #L152-L152, #L454-L454, #L579-L579: mark the Procfile, command, Dockerfile, and error-output blocks.
  • docs/xii/architecture/inventory/storage-call-sites.md#L52-L52: mark the bucket-type block.
  • docs/xii/architecture/inventory/task-call-sites.md#L43-L43, #L73-L73, #L152-L152, #L454-L454, #L579-L579: mark the command, task configuration, and error-output blocks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/inventory/cache-and-redis.md` at line 474, Add language
identifiers to every flagged Markdown fence to resolve MD040: use text for the
per-file breakdown in
docs/xii/architecture/inventory/cache-and-redis.md:474-474; use the appropriate
text or http identifiers for
docs/xii/architecture/inventory/frontend-file-flow.md:94-94 and :517-517; mark
the download route in docs/xii/architecture/inventory/plugin-impact.md:256-256;
mark the Procfile, command, Dockerfile, and error-output blocks in
docs/xii/architecture/inventory/runtime-and-deployment.md:43-43, 73-73, 152-152,
454-454, and 579-579; mark the bucket-type block in
docs/xii/architecture/inventory/storage-call-sites.md:52-52; and mark the
command, task configuration, and error-output blocks in
docs/xii/architecture/inventory/task-call-sites.md:43-43, 73-73, 152-152,
454-454, and 579-579.

Source: Linters/SAST tools

docs/xii/architecture/inventory/task-call-sites.md-110-124 (1)

110-124: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Narrow the report retry and orphan statement.

A normal ClientError from storage causes generate_and_upload_report to delete the row before re-raising. The orphan gap occurs when the process fails after the row save and before, or ambiguously during, the storage write. Describe the task as non-idempotent and potentially orphan-producing instead of stating that retries produce orphan rows unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/inventory/task-call-sites.md` around lines 110 - 124,
The Idempotency entry for generate_and_upload_report overstates orphan creation
by claiming retries produce orphan rows unconditionally. Revise it to retain the
non-idempotent classification while stating that retries can potentially produce
orphan rows when execution fails before or ambiguously during the storage write;
preserve the existing normal ClientError cleanup behavior.
🧹 Nitpick comments (8)
care/emr/tests/test_file_upload_api.py (1)

17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

response_content is now defined twice.

The same helper exists in care/emr/tests/test_storage_transport.py at lines 17-19, and care/emr/tests/test_file_upload_multipart.py inlines b"".join(...) in four places. Move it to a shared test utility so all three read the same way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/tests/test_file_upload_api.py` around lines 17 - 19, Move the
duplicated response_content helper into a shared test utility, then update
care/emr/tests/test_file_upload_api.py and
care/emr/tests/test_storage_transport.py to import and use it. Replace the four
inline b"".join(response.streaming_content) expressions in
test_file_upload_multipart.py with the shared helper, preserving the existing
streaming response behavior.
care/emr/api/viewsets/file_upload.py (2)

133-155: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The docstring and the field declarations disagree.

The docstring says FileUploadCreateSpec "remains the authoritative validator for the logical file type, category and filename" and that "this serializer does not restate those rules". The ChoiceField declarations then restate the type and category rules. Both layers now have to be updated together. Either drop the two ChoiceField constraints to plain CharField, or adjust the docstring to say that the choices are duplicated for schema generation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/api/viewsets/file_upload.py` around lines 133 - 155, Resolve the
contract mismatch in FileUploadMultipartSerializer by either changing file_type
and file_category to unconstrained CharField declarations so
FileUploadCreateSpec remains authoritative, or revising the docstring to
explicitly document the duplicated ChoiceField validation for schema generation;
keep the implementation and documentation consistent.

337-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The catch-all reports a storage failure for database failures too.

The try block covers put_object and both save calls. A failure in either save produces "Failed to upload file to storage", which is not what happened. Your own test test_database_failure_after_save_reports_failure in care/emr/tests/test_file_upload_multipart.py documents exactly this path. Narrow the block, or make the message honest.

♻️ Proposed narrowing
             try:
-                # The UploadedFile is handed straight to Django Storage; it is
-                # not read into memory first.
-                file_upload.files_manager.put_object(file_upload, uploaded_file)
-                file_upload.upload_completed = True
-                file_upload.updated_by = request.user
-                file_upload.save(skip_internal_name=True)
+                # The UploadedFile is handed straight to Django Storage; it is
+                # not read into memory first.
+                file_upload.files_manager.put_object(file_upload, uploaded_file)
             except Exception as e:
                 error_msg = "Failed to upload file to storage"
                 raise ValidationError(error_msg) from e
+
+            file_upload.upload_completed = True
+            file_upload.updated_by = request.user
+            file_upload.save(skip_internal_name=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/api/viewsets/file_upload.py` around lines 337 - 346, Update the
try/except around file upload handling so only
file_upload.files_manager.put_object is reported as a storage failure. Keep the
upload state updates and save calls outside that storage-specific exception
block, preserving test_database_failure_after_save_reports_failure’s
database-error behavior.
config/settings/base.py (2)

690-725: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm that suppressing endpoint_url for role-based credentials is intended.

_ROLE_BASED_BUCKET controls three options, but the comment only explains key and secret. Credential resolution and endpoint selection are independent concerns. A deployment that uses an instance role together with a VPC endpoint or an S3-compatible gateway loses its FILE_UPLOAD_BUCKET_ENDPOINT silently. If this matches the previous behavior, extend the comment to say so.

♻️ Optional: decouple endpoint from credential mode
-        endpoint_url=None if _ROLE_BASED_BUCKET else FILE_UPLOAD_BUCKET_ENDPOINT,
+        endpoint_url=FILE_UPLOAD_BUCKET_ENDPOINT or None,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/settings/base.py` around lines 690 - 725, Update the comment above
_ROLE_BASED_BUCKET to explicitly document whether endpoint_url is intentionally
omitted for role-based credentials, including that VPC or S3-compatible
endpoints are not preserved; if this suppression is not intended, decouple
endpoint_url from _ROLE_BASED_BUCKET while retaining the credential behavior.

668-684: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider validating the bucket names, not only the backend.

validate_storage_backend fails fast on a bad backend. The bucket names have no such check, and FILE_UPLOAD_BUCKET and FACILITY_S3_BUCKET both default to "". An empty alias bucket produces a provider error at the first upload instead of at startup. A short check here would keep the failure at configuration time, where it is much easier to read.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/settings/base.py` around lines 668 - 684, Validate
CARE_PATIENT_STORAGE_BUCKET, CARE_FACILITY_STORAGE_BUCKET, and
CARE_REPORT_STORAGE_BUCKET during configuration loading, rejecting empty or
whitespace-only values after applying their existing defaults. Reuse an
established bucket-name validation helper if available, and fail startup with a
clear configuration error before any upload occurs.
config/storage.py (1)

69-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The GCS branch drops S3 credentials without a signal.

If backend == "gcs", then region_name, access_key, secret_key and endpoint_url are discarded silently. config/settings/base.py always passes them. A deployer who switches the backend and keeps the old environment variables gets no hint that those values now do nothing. A short docstring line, or a log warning, would remove the guesswork.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/storage.py` around lines 69 - 78, The GCS branch in the storage
configuration silently ignores S3-specific settings. Add a concise docstring or
warning near the backend selection logic explicitly stating that region_name,
access_key, secret_key, and endpoint_url are unused when backend is "gcs",
without changing the existing GCS options behavior.
care/emr/tests/test_file_upload_multipart.py (1)

42-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Run ruff format on this file.

Lines 42 and 334 exceed the default 88-character limit, and the call at lines 90-92 is not in formatter shape. The repository guidelines require ruff check --fix . and ruff format . after code changes, so CI will most likely point at the same lines.

As per coding guidelines: "After code changes, run 'ruff check --fix .' and 'ruff format .' to lint and format Python code".

Also applies to: 90-92, 334-334

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@care/emr/tests/test_file_upload_multipart.py` at line 42, Run ruff check
--fix . followed by ruff format . to format
care/emr/tests/test_file_upload_multipart.py, including the long upload method
signature and the call around lines 90-92, without changing behavior.

Source: Coding guidelines

docs/xii/adr/ADR-0002-file-transport.md (1)

133-148: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Document the public-asset exception to authenticated downloads.

This ADR requires authenticated CARE downloads, but config/api_router.py adds unauthenticated facility-cover and user-profile-picture routes. The implementation is intentional, but the ADR currently gives a broader security guarantee than the code. State that these two asset classes are public exceptions and that file and report downloads remain authenticated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/adr/ADR-0002-file-transport.md` around lines 133 - 148, Update the
“Download Transport” section of ADR-0002 to explicitly identify facility covers
and user-profile pictures as intentional public exceptions served by
unauthenticated routes. Clarify that all file and report downloads continue to
require authenticated CARE endpoints, preserving the existing authenticated
download contract for those resources.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@care/emr/api/viewsets/file_assets.py`:
- Around line 26-41: Update PublicAssetView.serve to add long-lived
Cache-Control headers to the response returned by storage_file_response,
reflecting that public assets are immutable per object key and safe for
anonymous CDN or reverse-proxy caching.

In `@docs/xii/adr/ADR-0001-django-storage.md`:
- Around line 180-195: Synchronize transport documentation with the implemented
multipart-upload and CARE-mediated-download contract: in
docs/xii/adr/ADR-0001-django-storage.md lines 180-195 replace the
unchanged-transport statement, lines 307-315 reference the current
implementation document, and lines 319-346 mark transport work complete while
removing the claim that base64 uploads remain; in
docs/xii/architecture/00-scope-and-goals.md lines 354-364 remove or label
signed-upload flows as historical, and lines 633-649 replace the signed-URL
requirement with CARE-mediated downloads.

In `@docs/xii/architecture/00-scope-and-goals.md`:
- Around line 278-288: Update the distributed locks row in the default GCP
strategy table so PostgreSQL advisory locks are marked as pending rather than
advertised as the default until ADR-0005 prerequisites are satisfied.
Alternatively, only restore the default designation after completing the
required per-call-site analysis, concurrency evidence, and corresponding ADR and
implementation-status updates.

In `@docs/xii/architecture/01-current-runtime.md`:
- Around line 1215-1217: Align all listed inventories with the delivered
IS-01/ES-02 state: in docs/xii/architecture/01-current-runtime.md (lines
1215-1217), mark the S3 and signed-URL sections as historical or update them to
Django Storage and CARE routes; in
docs/xii/architecture/inventory/frontend-file-flow.md (lines 15-21), identify
§§1-10 as baseline and §12 as the current contract; in
docs/xii/architecture/inventory/runtime-and-deployment.md (lines 325-326), add
django-storages or label the dependency inventory as baseline; in
docs/xii/architecture/inventory/storage-call-sites.md (lines 456-459), remove
the claim that the deleted resolver remains used; and in
docs/xii/architecture/inventory/unresolved-items.md (lines 338-346 and 412-418),
close C2 with the private-bucket/anonymous-CARE-route decision and replace C10
with the current multipart/OpenAPI status.

In `@docs/xii/architecture/02-target-runtime.md`:
- Around line 529-533: The report generation path currently retries only
botocore ClientError, so GCS upload failures bypass generate_report_task’s retry
handling. Update the storage boundary to normalize provider-specific failures
into the exception type consumed by generate_report_task, or remove report
generation from the documented full-GCS production readiness claim; do not leave
this as an inventory-only exception.

In `@docs/xii/architecture/05-upstream-sync.md`:
- Around line 343-347: Update the pre-reset verification instructions around the
upstream sync procedure to check that local develop has a clean worktree,
inspect local-only commits with upstream/develop..develop, and compare
upstream/develop and local develop rather than origin/develop. After pushing,
add a two-way mirror-equality check, using an equivalent comparison such as git
diff --exit-code, before allowing the destructive reset.

In `@docs/xii/architecture/07-configuration-reference.md`:
- Around line 720-722: Update the test profile configuration in the documented
configuration reference to remove CARE_STORAGE_BACKEND=filesystem. Replace it
with the test-only InMemoryStorage override via override_settings, while keeping
production backend values limited to s3 and gcs.
- Around line 865-885: Update the local MinIO profile’s storage environment
variables to use the documented FILE_UPLOAD_*, FACILITY_S3_*, or shared BUCKET_*
names instead of unsupported S3_ENDPOINT_URL, S3_ACCESS_KEY, and S3_SECRET_KEY
entries. Apply the same replacement to the additional occurrence referenced in
the profile, preserving the existing alias-specific configuration behavior.
- Around line 167-182: Update the DJANGO_SETTINGS_MODULE examples and
accepted-value references in the configuration documentation, including the
sections around 4.1 and the additional GCP references, to use the supported
production module config.settings.deployment instead of config.settings.gcp.
Keep development references aligned with config.settings.local and ensure all
main contract/profile examples consistently use the repository policy.

In `@docs/xii/architecture/inventory/frontend-file-flow.md`:
- Around line 590-594: The documented file-upload flow must not allow clients to
create and complete records without verifying stored content. Update the section
describing POST /api/v1/files/ and mark_upload_completed to specify deprecation,
server-side completion, or a successful storage-existence check, and remove the
statement that leaves client-asserted completion as an accepted state.

In `@docs/xii/architecture/inventory/runtime-and-deployment.md`:
- Around line 118-120: Update the documented Redis TLS configuration to require
certificate verification using the deployment CA, and remove the production use
of ssl_cert_reqs=none. If disabling verification must remain, explicitly scope
it to local development only.

In `@docs/xii/architecture/inventory/task-call-sites.md`:
- Around line 35-37: Update the verified summary to distinguish three async-only
tasks—generate_report_task, send_totp_enabled_email, and
send_totp_disabled_email—from one mixed-dispatch task,
summarise_monetary_components, which also uses a recursive Cloud Tasks path.

In `@docs/xii/prompts/00-Complete-CARE-Runtime-Inventory`:
- Around line 41-52: Move the Phase 0 document contract from docs/gcp/ to the
docs/xii/ hierarchy throughout
docs/xii/prompts/00-Complete-CARE-Runtime-Inventory: update prerequisite
references at lines 41-52 to the current docs/xii/architecture/ paths, change
inventory outputs at lines 90-100 to docs/xii/architecture/inventory/, and
update verification and baseline output paths at lines 306-371 consistently;
apply the requested changes at all three sites in this file.

---

Minor comments:
In `@care/emr/resources/file_upload/spec.py`:
- Around line 107-115: Update FileUploadRetrieveSpec.perform_extra_serialization
to assign download_url only when obj.upload_completed is true, leaving it None
for incomplete uploads. Also update the download action that serves file uploads
to reject objects with upload_completed=false before resolving or returning the
storage URL.

In `@care/emr/tests/test_file_upload_multipart.py`:
- Around line 299-305: Correct test_incomplete_rows_remain_cleanable so its
setup and assertions match its intent: either rename it to describe the
completed multipart-upload invariant while preserving the current
successful-upload assertions, or create an incomplete upload row with
upload_completed=False and exercise cleanup_incomplete_file_uploads to verify it
remains cleanable. Do not leave the current misleading name and comment paired
with assertions for a completed upload.

In `@care/emr/utils/file_manager.py`:
- Around line 149-153: Annotate the mutable class-level dictionary
`_LEGACY_ALIASES` with `typing.ClassVar` to satisfy Ruff RUF012 while preserving
its shared read-only lookup behavior. Add the required typing import, then run
`ruff check --fix .` and `ruff format .`.

In `@docs/xii/adr/ADR-0001-django-storage.md`:
- Around line 81-83: Add language identifiers to the fenced code blocks in
ADR-0001, including the blocks around the S3Storage path and the configuration
values at the referenced locations. Use text for the storage path and env or
text for configuration-value blocks so all fences satisfy markdownlint MD040.

In `@docs/xii/adr/ADR-0002-file-transport.md`:
- Line 1: Remove the outer Markdown code fences wrapping the entire ADR,
including the opening fence at the document start and its matching closing fence
at the end. Preserve all inner fences that intentionally delimit examples so
headings and lists render as Markdown.

In `@docs/xii/architecture/02-target-runtime.md`:
- Around line 17-34: Correct the broken cross-document references across
docs/xii/architecture/02-target-runtime.md lines 17-34 and 1874-1880,
docs/xii/architecture/00-scope-and-goals.md lines 845-851,
docs/xii/architecture/03-migration-plan.md lines 9-13 and 2008-2014, and
docs/xii/architecture/04-testing.md lines 9-13 and 1960-1966. Replace the
outdated docs/gcp or docs/xii/gcp paths with the corresponding
docs/xii/architecture paths, preserving each link’s intended target and
relationship.

In `@docs/xii/architecture/04-testing.md`:
- Around line 1811-1835: Update the “Test Configuration” section to explicitly
define fake as a test-only CARE_TASK_BACKEND value, clarify that production
configuration rejects fake, and preserve the existing real-backend
integration-test guidance so the unit-test example does not conflict with the
runtime backend contract.
- Around line 1367-1376: Update the “23.2 Formatting and validation”
instructions to use recursive Terraform formatting across the full layout, and
document validation from deploy/gcp/terraform plus each environment/module
requiring initialization. Align the commands with the directory structure
described in 03-migration-plan.md so the stated all-modules and all-environments
coverage is accurate.

In `@docs/xii/architecture/05-upstream-sync.md`:
- Around line 10-15: Align every documentation reference in
docs/xii/architecture/05-upstream-sync.md with the approved docs/xii root:
update the depends_on entries at lines 10-15 to their corresponding
docs/xii/architecture paths, revise the search examples at lines 704-708, use
the docs/xii conflict-register path at lines 884-888, and set the next-document
reference at lines 1162-1168 to docs/xii/architecture/06-operations.md.

In `@docs/xii/architecture/06-operations.md`:
- Around line 9-15: Update all dependency and next-document references in
docs/xii/architecture/06-operations.md at lines 9-15 and 2527-2533, and
docs/xii/architecture/07-configuration-reference.md at lines 9-16 and 2842-2848,
replacing the incorrect docs/gcp/ paths with the corresponding
docs/xii/architecture/ paths.

In `@docs/xii/architecture/inventory/cache-and-redis.md`:
- Line 474: Add language identifiers to every flagged Markdown fence to resolve
MD040: use text for the per-file breakdown in
docs/xii/architecture/inventory/cache-and-redis.md:474-474; use the appropriate
text or http identifiers for
docs/xii/architecture/inventory/frontend-file-flow.md:94-94 and :517-517; mark
the download route in docs/xii/architecture/inventory/plugin-impact.md:256-256;
mark the Procfile, command, Dockerfile, and error-output blocks in
docs/xii/architecture/inventory/runtime-and-deployment.md:43-43, 73-73, 152-152,
454-454, and 579-579; mark the bucket-type block in
docs/xii/architecture/inventory/storage-call-sites.md:52-52; and mark the
command, task configuration, and error-output blocks in
docs/xii/architecture/inventory/task-call-sites.md:43-43, 73-73, 152-152,
454-454, and 579-579.

In `@docs/xii/architecture/inventory/storage-call-sites.md`:
- Around line 368-369: Update the migration totals in the summary to match the
documented call-site counts: use 9 migrated, 2 temporary wrappers, and 8 removed
for numbered sites 1–19, or explicitly define a broader scope and use its
corresponding 11 migrated, 2 temporary, and 8 removed totals including the two
unnumbered builders.

In `@docs/xii/architecture/inventory/task-call-sites.md`:
- Around line 110-124: The Idempotency entry for generate_and_upload_report
overstates orphan creation by claiming retries produce orphan rows
unconditionally. Revise it to retain the non-idempotent classification while
stating that retries can potentially produce orphan rows when execution fails
before or ambiguously during the storage write; preserve the existing normal
ClientError cleanup behavior.

In `@docs/xii/implementation/ES-02-file-transport-modernization.md`:
- Line 1: Remove the outer Markdown code fence surrounding the implementation
specification in “ES-02: File Transport Modernization,” including its opening
and closing delimiters, while preserving the document content and any intended
nested fences.
- Around line 95-109: Update the required-document list in
docs/xii/implementation/ES-02-file-transport-modernization.md lines 95-109 to
use the current docs/xii/architecture/, docs/xii/adr/, and
docs/xii/implementation/ paths. Also update the ADR checklist in lines 1044-1078
to reference docs/xii/adr/ADR-0002-file-transport.md.

---

Nitpick comments:
In `@care/emr/api/viewsets/file_upload.py`:
- Around line 133-155: Resolve the contract mismatch in
FileUploadMultipartSerializer by either changing file_type and file_category to
unconstrained CharField declarations so FileUploadCreateSpec remains
authoritative, or revising the docstring to explicitly document the duplicated
ChoiceField validation for schema generation; keep the implementation and
documentation consistent.
- Around line 337-346: Update the try/except around file upload handling so only
file_upload.files_manager.put_object is reported as a storage failure. Keep the
upload state updates and save calls outside that storage-specific exception
block, preserving test_database_failure_after_save_reports_failure’s
database-error behavior.

In `@care/emr/tests/test_file_upload_api.py`:
- Around line 17-19: Move the duplicated response_content helper into a shared
test utility, then update care/emr/tests/test_file_upload_api.py and
care/emr/tests/test_storage_transport.py to import and use it. Replace the four
inline b"".join(response.streaming_content) expressions in
test_file_upload_multipart.py with the shared helper, preserving the existing
streaming response behavior.

In `@care/emr/tests/test_file_upload_multipart.py`:
- Line 42: Run ruff check --fix . followed by ruff format . to format
care/emr/tests/test_file_upload_multipart.py, including the long upload method
signature and the call around lines 90-92, without changing behavior.

In `@config/settings/base.py`:
- Around line 690-725: Update the comment above _ROLE_BASED_BUCKET to explicitly
document whether endpoint_url is intentionally omitted for role-based
credentials, including that VPC or S3-compatible endpoints are not preserved; if
this suppression is not intended, decouple endpoint_url from _ROLE_BASED_BUCKET
while retaining the credential behavior.
- Around line 668-684: Validate CARE_PATIENT_STORAGE_BUCKET,
CARE_FACILITY_STORAGE_BUCKET, and CARE_REPORT_STORAGE_BUCKET during
configuration loading, rejecting empty or whitespace-only values after applying
their existing defaults. Reuse an established bucket-name validation helper if
available, and fail startup with a clear configuration error before any upload
occurs.

In `@config/storage.py`:
- Around line 69-78: The GCS branch in the storage configuration silently
ignores S3-specific settings. Add a concise docstring or warning near the
backend selection logic explicitly stating that region_name, access_key,
secret_key, and endpoint_url are unused when backend is "gcs", without changing
the existing GCS options behavior.

In `@docs/xii/adr/ADR-0002-file-transport.md`:
- Around line 133-148: Update the “Download Transport” section of ADR-0002 to
explicitly identify facility covers and user-profile pictures as intentional
public exceptions served by unauthenticated routes. Clarify that all file and
report downloads continue to require authenticated CARE endpoints, preserving
the existing authenticated download contract for those resources.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 743b46be-eaff-4220-b5ac-4395571c0d63

📥 Commits

Reviewing files that changed from the base of the PR and between 81149a3 and e6d928f.

⛔ Files ignored due to path filters (1)
  • Pipfile.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • Pipfile
  • care/emr/api/viewsets/file_assets.py
  • care/emr/api/viewsets/file_upload.py
  • care/emr/api/viewsets/report/report_upload.py
  • care/emr/models/file_upload.py
  • care/emr/models/report/report_upload.py
  • care/emr/reports/context_builder/data_points/fileupload.py
  • care/emr/reports/report_utils.py
  • care/emr/resources/file_upload/spec.py
  • care/emr/resources/report/report_upload/spec.py
  • care/emr/tasks/cleanup_incomplete_file_uploads.py
  • care/emr/tests/test_file_upload_api.py
  • care/emr/tests/test_file_upload_multipart.py
  • care/emr/tests/test_storage.py
  • care/emr/tests/test_storage_transport.py
  • care/emr/utils/file_download.py
  • care/emr/utils/file_manager.py
  • care/facility/models/facility.py
  • care/users/models.py
  • care/utils/csp/__init__.py
  • care/utils/csp/config.py
  • care/utils/file_uploads/cover_image.py
  • care/utils/tests/test_storage_config.py
  • config/api_router.py
  • config/settings/base.py
  • config/settings/config.py
  • config/storage.py
  • docs/xii/adr/ADR-0001-django-storage.md
  • docs/xii/adr/ADR-0002-file-transport.md
  • docs/xii/adr/ADR-0003-asynchronous-execution.md
  • docs/xii/adr/ADR-0004-configurable-application-cache.md
  • docs/xii/adr/ADR-0005-distributed-locking.md
  • docs/xii/adr/ADR-0006-portable-runtime-profiles.md
  • docs/xii/adr/ADR-0007-terraform-for-GCP.md
  • docs/xii/adr/ADR-0008-automated-continuous-integration.md
  • docs/xii/architecture/00-scope-and-goals.md
  • docs/xii/architecture/01-current-runtime.md
  • docs/xii/architecture/02-target-runtime.md
  • docs/xii/architecture/03-migration-plan.md
  • docs/xii/architecture/04-testing.md
  • docs/xii/architecture/05-upstream-sync.md
  • docs/xii/architecture/06-operations.md
  • docs/xii/architecture/07-configuration-reference.md
  • docs/xii/architecture/inventory/cache-and-redis.md
  • docs/xii/architecture/inventory/frontend-file-flow.md
  • docs/xii/architecture/inventory/plugin-impact.md
  • docs/xii/architecture/inventory/runtime-and-deployment.md
  • docs/xii/architecture/inventory/storage-call-sites.md
  • docs/xii/architecture/inventory/task-call-sites.md
  • docs/xii/architecture/inventory/unresolved-items.md
  • docs/xii/implementation/ES-01-storage.md
  • docs/xii/implementation/ES-02-file-transport-modernization.md
  • docs/xii/prompts/00-Complete-CARE-Runtime-Inventory
💤 Files with no reviewable changes (1)
  • care/utils/csp/config.py

Comment thread care/emr/api/viewsets/file_assets.py
Comment thread docs/xii/adr/ADR-0001-django-storage.md Outdated
Comment thread docs/xii/architecture/00-scope-and-goals.md
Comment thread docs/xii/architecture/01-current-runtime.md
Comment thread docs/xii/architecture/02-target-runtime.md
Comment thread docs/xii/architecture/07-configuration-reference.md
Comment thread docs/xii/architecture/inventory/frontend-file-flow.md Outdated
Comment thread docs/xii/architecture/inventory/runtime-and-deployment.md
Comment thread docs/xii/architecture/inventory/task-call-sites.md Outdated
Comment thread docs/xii/prompts/00-Complete-CARE-Runtime-Inventory
cesarbenjamindotnet and others added 3 commits August 7, 2026 13:42
Code:

- Public asset routes send a long-lived immutable Cache-Control.
  upload_cover_image mints a key per upload, so the bytes behind a key
  never change. Without this CARE serves every avatar view itself,
  which reading the bucket directly did not.
- FileUploadRetrieveSpec returns download_url only once upload_completed
  is set, and the download action refuses an incomplete row. A row
  exists before its bytes do, so the route could previously only 404.
- Annotate S3FilesManager._LEGACY_ALIASES as ClassVar (RUF012).
- Record that build_object_storage drops the S3 credential and endpoint
  arguments under gcs, and that _ROLE_BASED_BUCKET suppresses
  endpoint_url along with the key and secret.

Docs, where they contradicted the code:

- ADR-0001 still described transport as unchanged and listed presigned
  upload and download as future work; the IS-01 completion pass removed
  both. Only the base64 upload is left for IS-02.
- ADR-0002 promised authenticated downloads while two anonymous public
  asset routes exist. Records them as a bounded exception.
- 02-target-runtime makes the botocore ClientError retry gap a
  requirement rather than a note: under gcs the policy cannot fire, so
  a transient failure fails the report on its first attempt.
- scope-and-goals drops signed uploads and direct boto3 as reasons for
  a custom adapter, replaces the signed-URL expiry rule with
  CARE-mediated reads, and marks distributed locking undecided --
  advisory locks are session-scoped where CARE's lock is TTL-scoped.
- storage-call-sites totals were computed over an inconsistent
  denominator (11/2/6); now 11/2/8 over 21 sites, members listed. Drops
  the claim that the care/utils/csp resolvers survive; they are deleted.
- task-call-sites separates 3 always-async tasks from 1 mixed-dispatch,
  and narrows "retries produce orphan rows" to the actual window.
- runtime-and-deployment flags ssl_cert_reqs=none as a defect, and
  corrects the now-false django-storages and GCP-reference records.
- unresolved-items closes C2, updates C10, sharpens S2.
- 01-current-runtime gets a scoping note rather than a rewrite: it is a
  pinned baseline, and rewriting it would erase the reference point.

Mechanical: 54 stale docs/gcp paths -> docs/xii/architecture;
config.settings.gcp -> config.settings.deployment (no such module);
invented S3_* variables in the MinIO profile -> the real BUCKET_* names;
filesystem removed from the test storage profile; CARE_TASK_BACKEND=fake
marked test-only; terraform fmt -recursive with per-environment validate;
the pre-reset check compares against local develop, which is what the
reset destroys; 12 unlabeled code fences labelled.

Co-Authored-By: Claude <noreply@anthropic.com>
Code:

- Public asset routes send a long-lived immutable Cache-Control.
  upload_cover_image mints a key per upload, so the bytes behind a key
  never change. Without this CARE serves every avatar view itself,
  which reading the bucket directly did not.
- FileUploadRetrieveSpec returns download_url only once upload_completed
  is set, and the download action refuses an incomplete row. A row
  exists before its bytes do, so the route could previously only 404.
- Annotate S3FilesManager._LEGACY_ALIASES as ClassVar (RUF012).
- Record that build_object_storage drops the S3 credential and endpoint
  arguments under gcs, and that _ROLE_BASED_BUCKET suppresses
  endpoint_url along with the key and secret.

Docs, where they contradicted the code:

- ADR-0001 still described transport as unchanged and listed presigned
  upload and download as future work; the IS-01 completion pass removed
  both. Only the base64 upload is left for IS-02.
- ADR-0002 promised authenticated downloads while two anonymous public
  asset routes exist. Records them as a bounded exception.
- 02-target-runtime makes the botocore ClientError retry gap a
  requirement rather than a note: under gcs the policy cannot fire, so
  a transient failure fails the report on its first attempt.
- scope-and-goals drops signed uploads and direct boto3 as reasons for
  a custom adapter, replaces the signed-URL expiry rule with
  CARE-mediated reads, and marks distributed locking undecided --
  advisory locks are session-scoped where CARE's lock is TTL-scoped.
- storage-call-sites totals were computed over an inconsistent
  denominator (11/2/6); now 11/2/8 over 21 sites, members listed. Drops
  the claim that the care/utils/csp resolvers survive; they are deleted.
- task-call-sites separates 3 always-async tasks from 1 mixed-dispatch,
  and narrows "retries produce orphan rows" to the actual window.
- runtime-and-deployment flags ssl_cert_reqs=none as a defect, and
  corrects the now-false django-storages and GCP-reference records.
- unresolved-items closes C2, updates C10, sharpens S2.
- 01-current-runtime gets a scoping note rather than a rewrite: it is a
  pinned baseline, and rewriting it would erase the reference point.

Mechanical: 54 stale docs/gcp paths -> docs/xii/architecture;
config.settings.gcp -> config.settings.deployment (no such module);
invented S3_* variables in the MinIO profile -> the real BUCKET_* names;
filesystem removed from the test storage profile; CARE_TASK_BACKEND=fake
marked test-only; terraform fmt -recursive with per-environment validate;
the pre-reset check compares against local develop, which is what the
reset destroys; 12 unlabeled code fences labelled.

Co-Authored-By: Claude <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/xii/architecture/02-target-runtime.md (1)

1604-1617: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document CARE_STORAGE_BACKEND in the environment reference.

The storage architecture defines CARE_STORAGE_BACKEND as the selector for s3 and gcs, with s3 as the default. The core environment-variable list omits it. Add CARE_STORAGE_BACKEND=s3|gcs so operators can configure the documented GCS profile without searching other documents.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/architecture/02-target-runtime.md` around lines 1604 - 1617, Add
CARE_STORAGE_BACKEND=s3|gcs to the core environment-variable reference alongside
the other CARE_* backend selectors, documenting the default as s3 so operators
can select GCS.
docs/xii/adr/ADR-0002-file-transport.md (1)

54-58: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Limit the authentication exception to downloads.

Line 54 can be read as allowing unauthenticated uploads for public asset classes. Later text correctly requires authentication for asset writes. State this explicitly: all uploads require authentication; only downloads of facility cover images and user profile pictures may be unauthenticated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/xii/adr/ADR-0002-file-transport.md` around lines 54 - 58, Update the
file transport requirements under Authorization so every upload explicitly
requires authentication, while only downloads of facility cover images and user
profile pictures may be unauthenticated. Preserve the existing CARE mediation
and Django Storage API requirements.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/xii/architecture/inventory/task-call-sites.md`:
- Line 120: Update the idempotency statement near the ReportUpload flow to
narrow the retry claim: do not state that every retry duplicates both the row
and object. Explain that a normal ClientError from put_object triggers row
deletion, while an ambiguous storage-write failure can leave an orphan object
before a retry creates a new row and object; preserve the existing references to
report_utils.py:102 and report_utils.py:130.

---

Outside diff comments:
In `@docs/xii/adr/ADR-0002-file-transport.md`:
- Around line 54-58: Update the file transport requirements under Authorization
so every upload explicitly requires authentication, while only downloads of
facility cover images and user profile pictures may be unauthenticated. Preserve
the existing CARE mediation and Django Storage API requirements.

In `@docs/xii/architecture/02-target-runtime.md`:
- Around line 1604-1617: Add CARE_STORAGE_BACKEND=s3|gcs to the core
environment-variable reference alongside the other CARE_* backend selectors,
documenting the default as s3 so operators can select GCS.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75142627-3c14-49e9-939c-cc9b42daa4ee

📥 Commits

Reviewing files that changed from the base of the PR and between e6d928f and 0bde688.

📒 Files selected for processing (27)
  • care/emr/api/viewsets/file_assets.py
  • care/emr/api/viewsets/file_upload.py
  • care/emr/resources/file_upload/spec.py
  • care/emr/tests/test_file_upload_multipart.py
  • care/emr/tests/test_storage_transport.py
  • care/emr/utils/file_manager.py
  • config/settings/base.py
  • config/storage.py
  • docs/xii/adr/ADR-0001-django-storage.md
  • docs/xii/adr/ADR-0002-file-transport.md
  • docs/xii/architecture/00-scope-and-goals.md
  • docs/xii/architecture/01-current-runtime.md
  • docs/xii/architecture/02-target-runtime.md
  • docs/xii/architecture/03-migration-plan.md
  • docs/xii/architecture/04-testing.md
  • docs/xii/architecture/05-upstream-sync.md
  • docs/xii/architecture/06-operations.md
  • docs/xii/architecture/07-configuration-reference.md
  • docs/xii/architecture/inventory/cache-and-redis.md
  • docs/xii/architecture/inventory/frontend-file-flow.md
  • docs/xii/architecture/inventory/plugin-impact.md
  • docs/xii/architecture/inventory/runtime-and-deployment.md
  • docs/xii/architecture/inventory/storage-call-sites.md
  • docs/xii/architecture/inventory/task-call-sites.md
  • docs/xii/architecture/inventory/unresolved-items.md
  • docs/xii/implementation/ES-02-file-transport-modernization.md
  • docs/xii/prompts/00-Complete-CARE-Runtime-Inventory
🚧 Files skipped from review as they are similar to previous changes (19)
  • care/emr/resources/file_upload/spec.py
  • docs/xii/architecture/inventory/cache-and-redis.md
  • docs/xii/architecture/06-operations.md
  • care/emr/tests/test_storage_transport.py
  • docs/xii/architecture/01-current-runtime.md
  • docs/xii/prompts/00-Complete-CARE-Runtime-Inventory
  • care/emr/tests/test_file_upload_multipart.py
  • docs/xii/architecture/07-configuration-reference.md
  • docs/xii/architecture/04-testing.md
  • care/emr/utils/file_manager.py
  • care/emr/api/viewsets/file_assets.py
  • docs/xii/architecture/05-upstream-sync.md
  • config/settings/base.py
  • docs/xii/architecture/inventory/storage-call-sites.md
  • config/storage.py
  • docs/xii/architecture/inventory/plugin-impact.md
  • docs/xii/architecture/inventory/frontend-file-flow.md
  • docs/xii/implementation/ES-02-file-transport-modernization.md
  • docs/xii/architecture/03-migration-plan.md

Comment thread docs/xii/architecture/inventory/task-call-sites.md Outdated
response_content was defined identically in two test modules and inlined
as b"".join(...) in three more places. Move it to care/utils/tests/base
so all five read the same way.

Co-Authored-By: Claude <noreply@anthropic.com>
cesarbenjamindotnet and others added 3 commits August 7, 2026 14:20
…ages

# Conflicts:
#	care/emr/api/viewsets/file_upload.py
#	docs/xii/adr/ADR-0002-file-transport.md
#	docs/xii/architecture/inventory/frontend-file-flow.md
The line numbers carried over from the pre-IS-01 document and no longer
matched report_utils.py: internal_name is at 103, the put_object at
127-129, the cleanup delete at 133, LOCK_DURATION at 21.

Also sharpens the retry story. Saying "retries produce orphan rows"
conflated two different things: autoretry_for fires only on ClientError
from put_object, and that path deletes the row before re-raising, so a
retry does not accumulate rows. What it can accumulate is objects -- a
write ambiguous enough to raise after the bytes landed is never cleaned
up, and the retry writes another under a fresh key. Orphan rows need a
failure between the row save and the storage write.

Co-Authored-By: Claude <noreply@anthropic.com>
Merging the storage branch into the transport branch left documents
written against the pre-ES-02 tree describing code that had already
changed underneath them.

- ADR-0001 listed base64 upload as still outstanding and left the IS-02
  checkbox open. ES-02 delivered it; the persistence seam is unchanged.
- unresolved-items C10 claimed the upload endpoint takes base64 and
  carries no @extend_schema. It is multipart and annotated. Closed, and
  the stale line reference (230) corrected to 294.
- frontend-file-flow now states its own layering: 1-10 baseline, 11 the
  IS-01 record, 12 the current contract. The IS-02 rows in the 11.4
  table are marked closed rather than rewritten, so the IS-01 record
  survives.
- storage-call-sites section 11 opened by repeating a prediction the
  completion pass had already falsified.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant