learningequality / learningequality/studio
Publish an optional per-file upstream URL for large files
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 191
- Forks
- 307
- Avg merge
- 5d 6h
- Merged PRs (30d)
- 10
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
Studio records nothing about where a content file can be fetched from — Kolibri derives the URL from its own configuration, so every file is assumed to be served by Studio. Content files over 512 MiB are now mirrored to Cloudflare R2 and served from a different host. Store an upstream_url on File, set it for files above that threshold, and carry it into published channels and the public import-metadata API.
Complexity: Medium
Target branch: hotfixes
Context
kolibri_content/base_models.pyis a declared verbatim copy of Kolibri'sbase_models.py, andkolibri_public.models.LocalFileis a bare subclass of it. One field definition reaches both the published channel database and the public import-metadata API.publish.py:714is where a StudioFilebecomes akolibri_contentLocalFile, andmapper.py:225is where those rows reachkolibri_public.import_metadata_view.py:160validates the requestedschema_versionagainst the minimum and current versions, then selects fields from the current base model. A new field therefore appears in responses at every accepted version, not onlyVERSION_7.- Studio's vendored
CONTENT_SCHEMA_VERSIONisVERSION_6and tracks Kolibri's. The field arrives withVERSION_7. - Objects are copied into R2 by a GCS finalize trigger that filters at 512 MiB, so a
Filegiven anupstream_urlbelow that threshold would point at an object that was never copied. - Existing objects over the threshold are copied into R2 by a one-off infrastructure backfill, which has to have run before any existing
Filerow is given a URL.
The Change
Fileshould carry anupstream_urlrecording where the file can be fetched from.upstream_urlshould be a plain URL with no R2-specific meaning, so it can later hold a link to a host Studio never writes to.- The vendored
kolibri_contentschema should gain the field atVERSION_7, matching Kolibri's definition rather than diverging from it. - The field should reach both a published channel database and the public import-metadata API, since Kolibri imports through either.
- Existing rows should be populated without re-uploading or republishing, because the affected files are the largest ones we hold.
- The threshold should be a named constant with its coupling to the infrastructure trigger recorded, since a mismatch produces URLs pointing at objects that were never copied.
Out of Scope
- Adding the field to Kolibri's
base_models.pyand generating theVERSION_7schema. That lands in Kolibri first, and this issue vendors the result. - Copying objects into R2, and the one-off backfill of objects already over the threshold.
- Changing how files are uploaded to Studio — resumable uploads and the
file_sizewidening are learningequality/kolibri-ecosystem#53. - Populating
upstream_urlwith anything other than the R2 URL. Hosts Studio never writes to come later.
Acceptance Criteria
-
File.upstream_urlis nullable, and is set at upload for files above the mirroring threshold. - A file at or below the threshold is uploaded with
upstream_urlunset. -
LocalFile.upstream_urlis present in the vendoredVERSION_7schema in bothkolibri_contentandkolibri_public. - Publishing a channel writes each file's
upstream_urlinto the channel database. - A client requesting
schema_version=6from the import-metadata API still imports successfully with the field present in the response. - Existing
Filerows above the threshold haveupstream_urlpopulated by a backfill that is idempotent and resumable. - The backfill leaves rows at or below the threshold unchanged.
- The threshold constant matches the size filter on the infrastructure copy trigger.
- A channel published before this change still imports into Kolibri without error.
AI usage
I used Claude (Opus 5, via le-skills:writing-github-issues) to work through the design and draft this issue section by section. The decision to record where a file lives as data on the file, rather than making it an infrastructure concern, was mine, as was the field name. I had Claude read the publish and import-metadata paths to ground the constraints, and edited the drafts where they over-specified or got details wrong.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with kolibri_content/base_models.py and kolibri_public/models.py, then trace File conversion in publish.py:714, mapping in mapper.py:225, and field selection in import_metadata_view.py:160. Check the vendored schema_versions.py and the infrastructure trigger's 512 MiB filter. Done means the nullable field flows through uploads, publishing, both API paths, and an idempotent resumable backfill without breaking older imports.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, cloud, database
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100