Skip to content

Commit

Permalink
feat: serve public static files
Browse files Browse the repository at this point in the history
  • Loading branch information
MHajoha committed Aug 19, 2024
1 parent 2159c33 commit f8319ca
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 175 deletions.
59 changes: 59 additions & 0 deletions docs/qppe-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,50 @@ paths:
404:
description: Job not found.

/packages/{package_hash}/file/{namespace}/{short_name}/{path}:
parameters:
- $ref: '#/components/parameters/PackageHash'
- name: namespace
in: path
required: true
description: Namespace of the package from which to retrieve the file.
- name: short_name
in: path
required: true
description: Short name of the package from which to retrieve the file.
- name: path
in: path
required: true
description: Path within the package. Must start with either `static/` or `static-private/`.
schema:
type: string
pattern: "^(static|static-private)/.*$"
- $ref: '#/components/parameters/UserAgent'
- $ref: '#/components/parameters/AcceptLanguageOne'
post:
summary: Retrieve a static file from a package.
description: While the `package_hash` refers to the main package to load, that package may load other packages in
turn, in which case the path params `namespace` and `short_name` may identify one of those.
requestBody:
$ref: "#/components/requestBodies/MultipartWithOptionalPackage"
responses:
200:
description: Static file data
content:
"*/*": {}
404:
description: Package or its static file not found.
content:
application/json:
schema:
$ref: "#/components/schemas/NotFoundStatus"
500:
description: Error occurred.
content:
application/json:
schema:
$ref: "#/components/schemas/RequestError"

/package-extract-info:
parameters:
- $ref: '#/components/parameters/UserAgent'
Expand Down Expand Up @@ -457,6 +501,21 @@ components:
schema:
type: string

requestBodies:
MultipartWithOptionalPackage:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
package:
type: string
format: binary
encoding:
package:
contentType: application/zip, application/octet-stream

schemas:
PackageInfo:
type: object
Expand Down
174 changes: 0 additions & 174 deletions questionpy_server/api/routes.py

This file was deleted.

10 changes: 10 additions & 0 deletions questionpy_server/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from ._attempts import attempt_routes
from ._files import file_routes
from ._packages import package_routes
from ._status import status_routes

routes = (*attempt_routes, *file_routes, *package_routes, *status_routes)
78 changes: 78 additions & 0 deletions questionpy_server/api/routes/_attempts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>

from typing import TYPE_CHECKING

from aiohttp import web

from questionpy_common.environment import RequestUser
from questionpy_server.api.models import AttemptScoreArguments, AttemptStartArguments, AttemptViewArguments
from questionpy_server.decorators import ensure_package_and_question_state_exist
from questionpy_server.package import Package
from questionpy_server.web import json_response
from questionpy_server.worker.runtime.package_location import ZipPackageLocation

if TYPE_CHECKING:
from questionpy_server.app import QPyServer
from questionpy_server.worker.worker import Worker


attempt_routes = web.RouteTableDef()


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/start") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_start(
request: web.Request, package: Package, question_state: bytes, data: AttemptStartArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt = await worker.start_attempt(RequestUser(["de", "en"]), question_state.decode(), data.variant)

return json_response(data=attempt, status=201)


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/view") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_view(
request: web.Request, package: Package, question_state: bytes, data: AttemptViewArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt = await worker.get_attempt(
request_user=RequestUser(["de", "en"]),
question_state=question_state.decode(),
attempt_state=data.attempt_state,
scoring_state=data.scoring_state,
response=data.response,
)

return json_response(data=attempt, status=201)


@attempt_routes.post(r"/packages/{package_hash:\w+}/attempt/score") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_score(
request: web.Request, package: Package, question_state: bytes, data: AttemptScoreArguments
) -> web.Response:
qpyserver: "QPyServer" = request.app["qpy_server_app"]

package_path = await package.get_path()
worker: Worker
async with qpyserver.worker_pool.get_worker(ZipPackageLocation(package_path), 0, data.context) as worker:
attempt_scored = await worker.score_attempt(
request_user=RequestUser(["de", "en"]),
question_state=question_state.decode(),
attempt_state=data.attempt_state,
scoring_state=data.scoring_state,
response=data.response,
)

return json_response(data=attempt_scored, status=201)
38 changes: 38 additions & 0 deletions questionpy_server/api/routes/_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
# (c) Technische Universität Berlin, innoCampus <[email protected]>
from typing import TYPE_CHECKING

from aiohttp import web

from questionpy_server.decorators import ensure_package_and_question_state_exist
from questionpy_server.package import Package
from questionpy_server.worker.runtime.package_location import ZipPackageLocation

if TYPE_CHECKING:
from questionpy_server.app import QPyServer
from questionpy_server.worker.worker import Worker

file_routes = web.RouteTableDef()


@file_routes.post(r"/packages/{package_hash}/file/{namespace}/{short_name}/{path:static/.*}") # type: ignore[arg-type]
@ensure_package_and_question_state_exist
async def post_attempt_start(request: web.Request, package: Package) -> web.Response:
qpy_server: "QPyServer" = request.app["qpy_server_app"]
path = request.match_info["path"]

worker: Worker
async with qpy_server.worker_pool.get_worker(ZipPackageLocation(await package.get_path()), 0, None) as worker:
try:
# TODO: Support static files in non-main packages by using namespace and short_name.
file = await worker.get_static_file(path)
except FileNotFoundError:
return web.HTTPNotFound(reason="File not found.")

return web.Response(
body=file.data, content_type=file.mime_type,
# Set a lifetime of 1 year, i.e. effectively never expire. Since the package hash is part of the URL, cache
# busting is automatic.
headers={"Cache-Control": "public, immutable, max-age=31536000"}
)
Loading

0 comments on commit f8319ca

Please sign in to comment.