Skip to main content
Version: 3.0

File Upload

A FileUpload is the metadata record for a file living in S3-compatible object storage. You touch it whenever a Care resource needs an attachment — a patient document, an X-ray on an encounter, a signed consent form.

Two layers split the work. The Django model is storage: it persists the row and an opaque meta JSON column, nothing more. The Pydantic resource specs in care/emr/resources/file_upload/spec.py are the API — they define the file_type / file_category enums, validate the upload, decide what goes into meta (mime_type), and shape every read/write payload, including extension, signed_url, and read_signed_url, which are computed on the fly and have no backing columns.

Source:

Models

ModelPurpose
FileUploadMetadata record for a file stored in object storage (S3), associated with another Care resource by file_type + associating_id

FileUpload extends EMRBaseModel (shared Care EMR base with external_id, meta JSON, audit fields, and soft-delete semantics).

FileUpload fields

File metadata

FieldTypeNotes
nameCharField(2000)User-facing file name. Required on the API (FileUploadBaseSpec.name)
internal_nameCharField(2000)Storage object key. Set server-side from original_name on create, then a random key is generated on save(). Never set by clients
associating_idCharField(100)The external_id of the owning resource. Required (blank=False, null=False). There's no DB ForeignKey — integrity is enforced in the app layer
file_typeCharField(100)Domain of the owning resource. API-constrained to FileTypeChoices
file_categoryCharField(100)API-constrained to FileCategoryChoices
upload_completedBooleanFieldDefault False. Flips to True once the client finishes the direct-to-S3 upload — or immediately, on the inline upload-file path

meta JSON contents

meta is the opaque JSONField inherited from EMRBaseModel. The specs put exactly one structured key in it:

KeyTypeShape / Notes
mime_typestrMIME type of the file. Written in FileUploadCreateSpec.perform_extra_deserialization (obj.meta["mime_type"] = self.mime_type); read back during serialization via obj.meta.get("mime_type"). Validated against settings.ALLOWED_MIME_TYPES

Archival

FieldTypeNotes
is_archivedBooleanFieldDefault False
archive_reasonTextFieldBlank-able; set when archiving (required in the archive action body)
archived_datetimeDateTimeFieldNullable; set to timezone.now() when archived
archived_byFK → User (PROTECT)Nullable; related_name="archived_files"

Storage manager

FileUpload declares a class-level files_manager = S3FilesManager(BucketType.PATIENT). It's not a database field — it's the object-storage helper that mints signed write/read URLs and runs put_object, all against the PATIENT bucket.

Enums

FileTypeChoices values

The owning-resource domain (spec.py). String enum.

ValueLinks a file to
patientA patient
encounterAn encounter
consentA consent
diagnostic_reportA diagnostic report
service_requestA service request

FileCategoryChoices values

File category (spec.py). String enum.

Value
audio
xray
identity_proof
unspecified
discharge_summary
consent_attachment

Resource specs (API schema)

Every spec extends EMRResource (care/emr/resources/base.py), which supplies serialize (DB → Pydantic) and de_serialize (Pydantic → DB) plus the perform_extra_serialization / perform_extra_deserialization hooks. FileUpload deliberately omits __store_metadata__, so its hooks move mime_type in and out of meta by hand instead of leaning on the generic meta machinery.

SpecRoleExposes / behaviour
FileUploadBaseSpecshared baseid: UUID4 | None, name: str (required)
FileUploadCreateSpecwrite · createAdds original_name, file_type, file_category, associating_id, mime_type. Validates mime_type and original_name; on deserialize sets _just_created=True, internal_name = original_name, and meta["mime_type"]
FileUploadUpdateSpecwrite · updateSame fields as base (id, name) — the display name is the only thing you can edit
FileUploadListSpecread · listRead view (fields below); resolves extension, mime_type, audit users, uploaded_by, archived_by
FileUploadRetrieveSpecread · detailExtends FileUploadListSpec with signed_url, read_signed_url, internal_name; emits a write URL for just-created rows, otherwise a read URL
ConsentFileUploadCreateSpecwrite · create (consent)original_name + associating_id: UUID4; forces file_type=consent, file_category=consent_attachment server-side

Nothing here binds to a value set or uses CodeableConcept / Period / other common types — file_type and file_category are plain string enums.

FileUploadCreateSpec validation

FieldRule
nameRequired (inherited from base)
original_nameNon-empty; run through file_name_validator (care/utils/models/validators.py): ≤ 255 chars, must not start with ., must have an extension, extension must be in ALLOWED_FILE_EXTENSIONS and not in BLOCKED_FILE_EXTENSIONS
mime_typeMust be in settings.ALLOWED_MIME_TYPES, else "Invalid mime type"
file_typeMust be a FileTypeChoices member
file_categoryMust be a FileCategoryChoices member
associating_idRequired str

FileUploadListSpec read fields

FieldTypeSource
idUUID4obj.external_id (set in perform_extra_serialization)
namestrcolumn
file_typeFileTypeChoicescolumn
file_categoryFileCategoryChoicescolumn
associating_idstrcolumn
upload_completedboolcolumn
is_archivedbool | Nonecolumn
archive_reasonstr | Nonecolumn
archived_datetimedatetime | Nonecolumn
created_datedatetimecolumn
extensionstrderived via obj.get_extension()
mime_typestrobj.meta.get("mime_type")
uploaded_bydict | NoneUserSpec from cache (obj.created_by_id)
archived_byUserSpec | NoneUserSpec from cache (obj.archived_by_id)
created_by / updated_bydict | Noneserialize_audit_users

FileUploadRetrieveSpec extra fields

FieldTypeWhen populated
signed_urlstr | NoneWhen obj._just_created is truthy — a write (PUT) URL from files_manager.signed_url(obj). The upload target handed back at create time
read_signed_urlstr | NoneOtherwise — a read (GET) URL from files_manager.read_signed_url(obj)
internal_namestrThe storage object key (flagged in source as possibly not needing to be returned)

Methods & save behaviour

get_extension()

Derives the file extension from internal_name via parse_file_extension, formatted as .ext (or a multi-part form like .tar.gz), or "" when there's none. This is what surfaces to clients as the extension read field.

save() side effects

On save, a random internal_name is generated when internal_name is empty or the row is new (not self.id) and the caller has not passed skip_internal_name=True:

  1. A random internal_name is generated as uuid4() + int(time.time()).
  2. If an extension can be derived, it is appended to that random name.
  3. internal_name is set before the row is persisted.

Keeping the user-supplied name out of the storage key limits PII leakage if the bucket is compromised. The inline upload-file path calls save(skip_internal_name=True) after uploading, so the generated key survives.

API integration notes

Two upload flows

  1. Signed-URL (direct-to-S3), default: POST a FileUploadCreateSpec. The server validates, deserializes (_just_created=True), and returns FileUploadRetrieveSpec carrying a write signed_url. The client PUTs the bytes straight to S3, then calls POST .../{external_id}/mark_upload_completed/ to flip upload_completed=True.
  2. Inline base64 (POST .../upload-file/): the client sends original_name + base64 file_data. The server decodes it, enforces settings.MAX_FILE_UPLOAD_SIZE (MB), sniffs the real MIME type with python-magic, re-checks ALLOWED_MIME_TYPES, builds a FileUploadCreateSpec, sets _just_created=False, uploads via files_manager.put_object, sets upload_completed=True, and returns FileUploadRetrieveSpec — so the response carries read_signed_url, not a write URL.

Server-maintained behaviour

  • internal_name is platform-maintained — never set it from a client; the model generates a random PII-free key.
  • mime_type lives in meta, not a column. FileUploadCreateSpec writes it and serialization reads it back. On the inline path it is re-derived from file content (magic.from_buffer) rather than trusted from the client.
  • associating_id + file_type together link a file to its owning resource. list requires both as query params and returns only upload_completed=True rows.
  • Every action runs file_authorizer(user, file_type, associating_id, "read"|"write").
  • Archival (POST .../{external_id}/archive/, body { archive_reason }) sets is_archived, archive_reason, archived_datetime, archived_by server-side and returns FileUploadListSpec. This soft state is separate from the EMRBaseModel soft-delete (deleted).
  • mark_upload_completed and archive both return FileUploadListSpec (no signed URL).