diff --git a/system-server/Pipfile b/system-server/Pipfile index 78c13a0ff55..d1ce7f43f6a 100644 --- a/system-server/Pipfile +++ b/system-server/Pipfile @@ -14,6 +14,7 @@ pydantic = "==1.10.12" importlib-metadata = ">=4.13.0,<5" sqlalchemy = "==1.4.51" pyjwt = "==2.6.0" +filetype = "==1.2.0" systemd-python = { version = "==234", markers="sys_platform == 'linux'" } server-utils = {editable = true, path = "./../server-utils"} system_server = {path = ".", editable = true} diff --git a/system-server/Pipfile.lock b/system-server/Pipfile.lock index bbaa48e640c..d7d315362f2 100644 --- a/system-server/Pipfile.lock +++ b/system-server/Pipfile.lock @@ -50,6 +50,14 @@ "markers": "python_version >= '3.7'", "version": "==0.99.1" }, + "filetype": { + "hashes": [ + "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", + "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25" + ], + "index": "pypi", + "version": "==1.2.0" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -231,12 +239,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "uvicorn": { "hashes": [ diff --git a/system-server/system_server/system/oem_mode/router.py b/system-server/system_server/system/oem_mode/router.py index c8c6d96240b..0f3b9aa52f4 100644 --- a/system-server/system_server/system/oem_mode/router.py +++ b/system-server/system_server/system/oem_mode/router.py @@ -1,6 +1,17 @@ """Router for /system/register endpoint.""" -from fastapi import APIRouter, Depends, status, Response +import os +import filetype # type: ignore[import-untyped] +from fastapi import ( + APIRouter, + Depends, + status, + Response, + UploadFile, + File, + HTTPException, +) + from .models import EnableOEMMode from ...settings import SystemServerSettings, get_settings, save_settings @@ -35,3 +46,87 @@ async def enable_oem_mode_endpoint( except Exception: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return response + + +@oem_mode_router.post( + "/system/oem_mode/upload_splash", + summary="Upload an image to be used as the boot up splash screen.", + responses={ + status.HTTP_201_CREATED: {"message": "OEM Mode splash screen uploaded"}, + status.HTTP_400_BAD_REQUEST: {"message": "OEM Mode splash screen not set"}, + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: { + "message": "File is larger than 5mb" + }, + status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: {"message": "Invalid file type"}, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + "message": "OEM Mode splash unhandled exception." + }, + }, +) +async def upload_splash_image( + response: Response, + file: UploadFile = File(...), + settings: SystemServerSettings = Depends(get_settings), +) -> Response: + """Router for /system/oem_mode/upload_splash endpoint.""" + # Make sure oem mode is enabled before this request + if not settings.oem_mode_enabled: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="OEM Mode needs to be enabled to upload splash image.", + ) + + # Get the file info + file_info = filetype.guess(file.file) + if file_info is None: + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unable to determine file type", + ) + + # Only accept PNG files + accepted_file_types = ["image/png", "png"] + content_type = file_info.extension.lower() + if ( + file.content_type not in accepted_file_types + or content_type not in accepted_file_types + ): + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unsupported file type", + ) + + file_size = 0 + for chunk in file.file: + file_size += len(chunk) + if file_size > 5 * 1024 * 1024: # 5MB + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail="File is larger than 5mb.", + ) + + # TODO: Validate image dimensions + + # return the pointer back to the starting point so that the next read starts from the starting point + await file.seek(0) + + try: + # Remove the old image if exists + if settings.oem_mode_splash_custom: + os.unlink(settings.oem_mode_splash_custom) + + # file is valid, save to final location + filepath = f"{settings.persistence_directory}/{file.filename}" + with open(filepath, "wb+") as f: + f.write(file.file.read()) + + # store the file location to settings and save the dotenv + settings.oem_mode_splash_custom = filepath + success = save_settings(settings) + response.status_code = ( + status.HTTP_201_CREATED if success else status.HTTP_400_BAD_REQUEST + ) + except Exception: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + return response diff --git a/system-server/tests/integration/resources/oem_mode_custom.png b/system-server/tests/integration/resources/oem_mode_custom.png new file mode 100644 index 00000000000..14cf4ac12bd Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_custom.png differ diff --git a/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png b/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png new file mode 100644 index 00000000000..2cc51a01cb0 Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png differ diff --git a/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg b/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg new file mode 100644 index 00000000000..aa95d031d93 Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg differ diff --git a/system-server/tests/integration/test_oem_mode.tavern.yaml b/system-server/tests/integration/test_oem_mode.tavern.yaml index 399422c96b8..9778495c6c3 100644 --- a/system-server/tests/integration/test_oem_mode.tavern.yaml +++ b/system-server/tests/integration/test_oem_mode.tavern.yaml @@ -1,5 +1,5 @@ --- -test_name: PUT Enable OEM Mode +test_name: Test enable/disable OEM Mode marks: - usefixtures: - run_server @@ -34,4 +34,91 @@ stages: content-type: application/json response: status_code: 422 +--- +test_name: Upload, and validate a good image for OEM Mode + +marks: + - usefixtures: + - run_server +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: &upload_splash_first + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 + +--- +test_name: Dont process upload_splash request if oem mode is disabled + +marks: + - usefixtures: + - run_server + +stages: + - name: Disable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": false + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 403 + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 +--- +test_name: Validate the image before processing + +marks: + - usefixtures: + - run_server +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload non-PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_wrong_image_type.jpeg' + response: + status_code: 415 + - name: Upload a PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201