Browse the file manager ("Dateien") as a filesystem

Many teachers never use topics or boards; their material sits in the
course's file area, and the tools answered "0 files" for courses holding
dozens of worksheets — 21 of 26 courses on the live account. Persönliche,
Kurs-, Team- and Geteilte Dateien live in the legacy file store, not in
files-storage, and its service is not in the public ingress. The only way in
is the legacy client: HTML listings, and GET /files/signedurl for a
pre-signed download.

core/legacy-files.ts turns that into one path tree — /my, /courses/<course>,
/teams/<team>, /shared — resolving names that contain "/", ids anywhere in a
path, and wrong or ambiguous names with a message saying what is there. A
listing that does not parse throws; it never reads as an empty folder.

Some of the legacy client's GET routes write (GET /files/share/ mints a
share token), so getFileManagerPage allows only the listing routes, by
pattern. Signed URLs are fetched with no credentials and must be https.

- MCP: fs_list, fs_tree, fs_find and fs_read; get_course lists course files.
- CLI: schulcloud fs ls, tree, find and get, recursive and resumable.
- API: /api/fs/list, tree, find and file.
- Index: the crawl walks the file manager (INDEX_FILE_MANAGER, on by
  default), so search covers the text inside those files and sync mirrors
  them under <course>/Kurs-Dateien.

The local instance gains a fixture for all four areas. It needed a loopback,
so signed URLs open from the host, and a pre-created bucket, since MinIO
does not implement PutBucketCors.

135 tests. Smoke 55/55 live; 57/57 and 55/55 on the local instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-16 20:19:16 +02:00
parent 10c6544579
commit bed3923902
32 changed files with 2696 additions and 106 deletions

View File

@@ -89,6 +89,7 @@ Every one of these is marked in the env files at the line it affects.
```
docker-compose.yml the stack; profiles: (default) | tools | av | preview
(minio-loopback: see "Simulating a teacher")
env/ one file per service, all values local-only
proxy/nginx.conf GENERATED — the single origin, see scripts/gen-proxy-conf.py
scripts/seed.sh loads the demo school via the management app
@@ -183,14 +184,40 @@ node scripts/simulate-teacher.mjs create # course, room, topic, task, board,
# Etherpad pad, folder, files
node scripts/simulate-teacher.mjs update # rename and rewrite all of it
node scripts/simulate-teacher.mjs delete # remove it again
node scripts/simulate-teacher.mjs files # only the file-manager part, onto an existing fixture
```
It also fills the **file manager** ("Dateien") — the legacy file system behind
Persönliche, Kurs-, Team- and Geteilte Dateien, a separate store from the board
files above — with a course folder tree ("Arbeitsblätter/Woche 1"), a team
folder, a folder in the demo student's own files, and a teacher's file shared
read-only with the student. The student is added to the adopted team so the
team files are visible from the account under test. Every file carries a unique
search term, so search over the index can be checked per area.
Legacy file uploads only work because of two changes to the stack:
- **`minio-loopback`.** The legacy service signs upload and download URLs for
its storage provider's single endpoint, and uses that endpoint for its own S3
calls. `minio:9000` works inside the compose network and nowhere else, so the
browser and the MCP server on the host were handed URLs they could not open.
`seed.sh` now registers `http://localhost:9900`, and this socat sidecar,
sharing the api container's network namespace, forwards that address to
MinIO — the same url then works from the api, the browser and the host.
After recreating `api`, recreate `minio-loopback` too: it lives in api's
network namespace.
- **The school bucket is created up front.** The legacy service makes
`bucket-<schoolId>` on first upload and then calls `PutBucketCors`, which
MinIO does not implement, so the first upload failed with *"A header you
provided implies functionality that is not implemented"*. `minio-init.sh`
creates the demo school's bucket, and an existing bucket skips both calls.
Ids are kept in `.simulate-teacher.json` between phases, so the MCP server can
be pointed at the instance in between:
```bash
eval "$(./scripts/mcp-env.sh)" # as the demo student
cd .. && npm run smoke # 39 checks against the local instance
cd .. && npm run smoke # 57 checks against the local instance
```
`mcp-env.sh` points the index at its own database, `schulcloud_local`, and the