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

@@ -50,6 +50,39 @@ alongside courses, with their files under the room's name rather than a course's
what changed: that is a handful of requests, where a full re-crawl reads every
course. The server refuses a repeat within a minute unless you pass `--force`.
### The file manager (`fs`)
The Schulcloud file manager ("Dateien") — Persönliche, Kurs-, Team- and
Geteilte Dateien — browsed like a filesystem, live:
```
schulcloud fs ls [path] [--long]
schulcloud fs tree [path] [--depth <n>] [--max-folders <n>]
schulcloud fs find <name> [--path <path>] [--type file|folder] [--long]
schulcloud fs get <path> [--out <path>] [--force] [--jobs <n>]
```
```console
$ schulcloud fs ls /courses
$ schulcloud fs tree "/courses/FIA24B - SK (Rh)"
$ schulcloud fs find "*Erben*" --path /courses
$ schulcloud fs get "/courses/FIA24B - SK (Rh)/02_Erbrecht" --out ~/Erbrecht
```
The tree is `/my`, `/courses/<course>`, `/teams/<team>` and `/shared`; the
German names ("/Kurs-Dateien") work too. Names may contain `/` — course names
often do — and still resolve; any segment may also be an id from `--long`.
`fs find` matches any part of a name, or, given `*` or `?`, the whole name as
`find -name` does. `fs get` on a folder downloads everything below it, keeps
the structure, and skips files already present at the same size — so re-running
it resumes. Each folder is one page load on the server, so large trees take a
while.
`sync` mirrors these files too, under `<course>/Kurs-Dateien/…`,
`Persönliche Dateien/…`, `Team-Dateien/<team>/…` and `Geteilte Dateien/`, once
the server's index includes them (`INDEX_FILE_MANAGER`, on by default).
## How sync works
It is a **one-way mirror, not a two-way sync**, and that follows from the data