docs: imported courses in the studio (media kept out of git, moved to <data>/media/<id>)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 20:35:36 +01:00
co-authored by Claude Opus 5.5
parent 4a49e28fcc
commit f834088588
2 changed files with 44 additions and 2 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ Layout after the review (controller ruling on I4):
Proposal: main.ts calls `createCourseRepository(studioCourseDirs(config))`, or `loadConfig` appends both dirs.
Imported courses (`source.kind` other than `authored`, with their root under `<dataDir>/courses`) are read-only in the studio for v1, with the message "Imported course: read-only in the studio for now". There is a `TODO(studio)` in `feature.ts` (`readOnlyMessage`) for converting one into a studio-managed repo. The studio refuses to create a course whose id already exists as `<dataDir>/courses/<id>`. WP-B should likewise refuse an import id that `<dataDir>/studio/repos/<id>.git` already uses.
Imported courses (`source.kind` other than `authored`, with their root under `<dataDir>/courses`) are read-only in the studio for v1, with the message "Imported course: read-only in the studio for now". There was a `TODO(studio)` in `feature.ts` (`readOnlyMessage`) for converting one into a studio-managed repo; v1.1 S resolved it with the owner-only "Make editable in the studio" action (see `docs/operations.md`, "Imported courses in the studio"). The studio refuses to create a course whose id already exists as `<dataDir>/courses/<id>`. WP-B should likewise refuse an import id that `<dataDir>/studio/repos/<id>.git` already uses.
## 3. Live preview: swap in the learner `LessonView`
+43 -1
View File
@@ -22,7 +22,7 @@ Mounts (all course mounts are read-only; STCourses never changes your originals)
| Host | Container | What |
|---|---|---|
| volume `stc-data` | `/data` | database (SQLite), imported courses (`/data/courses`), studio repos (`/data/studio/repos`) and their published worktrees (`/data/worktrees`), caches (`/data/cache`), AI drafts for non-studio courses (`/data/drafts`) |
| volume `stc-data` | `/data` | database (SQLite), imported courses (`/data/courses`), studio repos (`/data/studio/repos`) and their published worktrees (`/data/worktrees`), media of imports made editable in the studio (`/data/media`), caches (`/data/cache`), AI drafts for non-studio courses (`/data/drafts`) |
| `STC_COURSES_HOST` (default `./courses`) | `/courses` | course repos, one folder with a `stcourse.yaml` per course |
| `STC_IMPORT_HOST` (default `./import-inbox`) | `/import` | files the owner may import (Packt zips, video folders, PDFs, code zips) |
@@ -78,6 +78,48 @@ Without Docker: `bun install`, then `STC_SECRET=… NODE_ENV=production bun apps
- Course media (`/api/media/…`) is sent with `X-Content-Type-Options: nosniff`, and SVG, HTML and JavaScript files
with `Content-Security-Policy: sandbox`, so author content cannot run script on the app's origin.
## Imported courses in the studio
Imported courses (`<data>/courses/<id>`) are read-only in the studio until the owner chooses **Make editable in the
studio** on the course's studio page. That runs a background job (`studio.convert-import`) which:
1. commits the manifest, lessons, transcripts, small images, small PDFs and code into a new studio repo,
`<data>/studio/repos/<id>.git`, checked out as `<data>/worktrees/<id>/published`. The manifest is committed
unchanged: the course keeps its id, its visibility (restricted after an import) and its `source.kind`, so only the
owner can later make it public or signed-in. ACL entries, progress and notes are keyed by course and lesson id
and are kept;
2. keeps **large media out of git**: every video or audio file (`mp4 m4v webm mov mkv avi mp3 m4a wav ogg oga opus
flac`) and any other file over 8 MiB. Each is listed with its path, size and SHA-256 in `.stcourses/media.json`
in the repo and excluded by one anchored line in the repo's root `.gitignore` (a `.gitignore` the course already
had is kept above that block). Git LFS is not used: it is not in the image and would need an LFS server;
3. **moves the import folder to `<data>/media/<id>`** (decided over keeping it in place: a manifest left in
`<data>/courses/<id>` would be found as a second course with the same id, and the importer's "add to course" and
transcription jobs would keep changing a folder learners no longer see). `<data>/media` is never scanned for
courses and nothing writes to it afterwards. File permissions are left alone, because an import made in `link`
mode shares its files with your originals;
4. puts every referenced file into the published worktree, and later into each author's draft worktree, as a
**hard link** to `<data>/media/<id>/<path>` (a copy if they are on different filesystems). The manifest paths are
unchanged, so `/api/media/<id>/…` serves the same URLs with Range support, and no extra disk space is used.
In the studio, referenced media cannot be replaced or deleted (save, upload and conflict resolution refuse them with
409); upload a new file under another name instead. A studio write never goes through a hard link: a file with more
than one link is replaced, not overwritten.
Interrupted conversions are safe to retry, and a job left running by a restart is retried automatically: if no
published worktree was made, the partial repo is removed and the conversion starts again; if the repo exists but the
import folder was not moved yet, only the move is done.
Things to know:
- Back up `<data>/media` together with `<data>/studio`: the repo alone does not contain the videos.
- **Sync** pushes only the git repo. A clone of the remote has the manifest, lessons and `.stcourses/media.json`, but
not the media files themselves; copy `<data>/media/<id>` alongside if you move the course to another server.
- If a worktree loses a media file (for example after a manual `git clean -x`), the next publish links it back into
the published worktree, and a new draft worktree gets every file. A file missing from `<data>/media/<id>` shows
as a course diagnostic ("lesson file not found"), never a crash.
- After conversion the importer can no longer add decks, code or transcripts to that course (it only edits
`<data>/courses`). Add them in the studio.
## Memory budget
The deployment must stay under 2 GB idle. Measured with this compose file (`--profile postgres`), the image built