Files
MechaCat02 bed3923902 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>
2026-09-16 20:19:16 +02:00

110 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Seed the local instance with the upstream demo school.
#
# This is what the real deployment's init job does (dof_app_deploy
# .../schulcloud-server-init/templates/configmap_file_init.yml.j2): it asks the
# management app to load backup/setup/*.json, which ships inside the server
# image. Safe to re-run — collections are replaced, not appended to.
set -euo pipefail
cd "$(dirname "$0")/.."
MGMT=http://127.0.0.1:3333/api/management/database
COMPOSE=(docker compose)
echo "==> waiting for the management app"
for _ in $(seq 1 60); do
if curl -fsS -o /dev/null "$MGMT/../../docs" 2>/dev/null || curl -fsS -o /dev/null -X POST "$MGMT/sync-indexes" 2>/dev/null; then
break
fi
sleep 5
done
echo "==> seeding collections (this takes a minute or two)"
curl -fsS --retry 30 --retry-all-errors --retry-delay 10 \
-X POST "$MGMT/seed?with-indexes=true" >/dev/null
echo " done"
# The legacy file service (course/topic attachments, as opposed to the newer
# /api/v3/file API) reads its S3 credentials from a storageproviders document
# rather than from the environment, and there is deliberately no seed data for
# it. Without this, legacy uploads fail with a provider-not-found error.
echo "==> registering MinIO as the legacy storage provider"
S3_KEY=$(grep -E '^S3_KEY=' env/shared.env | cut -d= -f2-)
SECRET=$(curl -fsS -X POST "$MGMT/encrypt-plain-text" \
-H 'Content-Type: application/json' \
-d "$(printf '{"plainText":"miniouser","key":"%s"}' "$S3_KEY")")
"${COMPOSE[@]}" exec -T mongo mongosh schulcloud --quiet --eval "
const id = ObjectId('62949a4003839b6162aa566b');
db.storageproviders.replaceOne({ _id: id }, {
_id: id, isShared: true, region: 'eu-central-1', type: 'S3',
// Not minio:9000: this one endpoint also goes into every signed URL, and
// those are opened by the browser and the MCP server on the host. The
// minio-loopback service makes localhost:9900 reach MinIO from the api too.
endpointUrl: 'http://localhost:9900',
accessKeyId: 'miniouser',
secretAccessKey: '$SECRET',
maxBuckets: 150, freeBuckets: 138,
createdAt: new Date(), updatedAt: new Date(), __v: 0,
}, { upsert: true });
const r = db.schools.updateMany({}, { \$set: { storageProvider: id } });
print(' schools linked to the provider: ' + r.modifiedCount);
"
# The demo data is dated 2017-2018, and the v3 endpoints filter on those dates:
# a course that has ended shows a student no tasks, and a task whose due date
# passed years ago is in neither the open nor the finished list and has dropped
# off the course page. The result is a student account that looks empty for
# reasons that have nothing to do with whatever is being tested against it.
#
# So move the stale data into the present. Courses get a term around today.
# Homework is shifted by one offset per date cluster, which keeps the relative
# order — and so which tasks are past due, the ones carrying the graded
# submissions — while landing the newest of them two weeks ago.
echo "==> dating the demo data to the present"
"${COMPOSE[@]}" exec -T mongo mongosh schulcloud --quiet --eval "
const now = new Date();
const courses = db.courses.updateMany({ untilDate: { \$lt: now } }, { \$set: {
startDate: new Date(now.getTime() - 180 * 86400000),
untilDate: new Date(now.getTime() + 185 * 86400000),
} });
print(' courses given a current term: ' + courses.modifiedCount);
let moved = 0;
for (let pass = 0; pass < 10; pass++) {
const cutoff = new Date(Date.now() - 365 * 86400000);
const newest = db.homeworks.find({ dueDate: { \$lt: cutoff } }).sort({ dueDate: -1 }).limit(1).toArray()[0];
if (!newest) break;
const offset = (Date.now() - 14 * 86400000) - newest.dueDate.getTime();
db.homeworks.find({ dueDate: { \$lt: cutoff } }).forEach((h) => {
const set = {};
for (const field of ['dueDate', 'availableDate', 'createdAt', 'updatedAt']) {
if (h[field] instanceof Date) set[field] = new Date(h[field].getTime() + offset);
}
db.homeworks.updateOne({ _id: h._id }, { \$set: set });
moved++;
});
}
print(' homework brought forward: ' + moved);
"
cat <<'ACCOUNTS'
==> ready — http://localhost:4400
Seeded accounts (the demo password differs by account — upstream quirk):
demo-schueler@schul-cloud.org student Fritz Schmidt schulcloud
^ has graded submissions, incl. a feedback-only and a 100% one
demo-lehrer@schul-cloud.org teacher Erika Meier schulcloud
klara.fall@schul-cloud.org teacher Klara Fall Schulcloud1!
^ owns Fritz's graded Biologie submissions
lehrer@schul-cloud.org teacher Cord Carl Schulcloud1!
admin@schul-cloud.org admin Thorsten Test Schulcloud1!
*.qa@schul-cloud.org various Schulcloud1qa!
Sign in as the teacher to grade, as the student to see what grading looks
like from the side our MCP server reads.
ACCOUNTS