team-3/src/c3nav/mesh/newapi.py

144 lines
5.8 KiB
Python
Raw Normal View History

from datetime import datetime
2023-11-17 16:42:27 +01:00
from django.db import IntegrityError, transaction
2023-11-11 13:30:12 +01:00
from ninja import Field as APIField
from ninja import Router as APIRouter
from ninja import Schema, UploadedFile
from ninja.pagination import paginate
from pydantic import validator
2023-11-17 16:42:27 +01:00
from c3nav.api.exceptions import APIConflict, APIRequestValidationFailed
from c3nav.api.newauth import BearerAuth, auth_permission_responses, auth_responses
from c3nav.mesh.dataformats import BoardType
from c3nav.mesh.messages import ChipType
from c3nav.mesh.models import FirmwareVersion
api_router = APIRouter(tags=["mesh"])
class FirmwareBuildSchema(Schema):
id: int
variant: str = APIField(..., example="c3uart")
chip: ChipType = APIField(..., example=ChipType.ESP32_C3.name)
sha256_hash: str = APIField(..., regex=r"^[0-9a-f]{64}$")
url: str = APIField(..., alias="binary", example="/media/firmware/012345/firmware.bin")
boards: list[BoardType] = APIField(..., example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
@staticmethod
def resolve_chip(obj):
# todo: do this in model? idk
return ChipType(obj.chip)
2023-11-17 16:42:27 +01:00
@staticmethod
def resolve_boards(obj):
print(obj.boards)
return obj.boards
class FirmwareSchema(Schema):
id: int
project_name: str = APIField(..., example="c3nav_positioning")
version: str = APIField(..., example="499837d-dirty")
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
created: datetime
builds: list[FirmwareBuildSchema]
@validator('builds')
def builds_variants_must_be_unique(cls, builds):
if len(set(build.variant for build in builds)) != len(builds):
raise ValueError("builds must have unique variant identifiers")
return builds
class Error(Schema):
detail: str
2023-11-14 18:29:21 +01:00
@api_router.get('/firmwares/', summary="List available firmwares",
response={200: list[FirmwareSchema], **auth_responses})
@paginate
def firmware_list(request):
return FirmwareVersion.objects.all()
2023-11-14 18:29:21 +01:00
@api_router.get('/firmwares/{firmware_id}/', summary="Get specific firmware",
response={200: FirmwareSchema, **auth_responses})
def firmware_detail(request, firmware_id: int):
try:
return FirmwareVersion.objects.get(id=firmware_id)
except FirmwareVersion.DoesNotExist:
return 404, {"detail": "firmware not found"}
class UploadFirmwareBuildSchema(Schema):
variant: str = APIField(..., example="c3uart")
chip: ChipType = APIField(..., example=ChipType.ESP32_C3.name)
sha256_hash: str = APIField(..., regex=r"^[0-9a-f]{64}$")
boards: list[BoardType] = APIField(..., example=[BoardType.C3NAV_LOCATION_PCB_REV_0_2.name, ])
2023-11-17 16:42:27 +01:00
project_description: dict = APIField(..., title='project_description.json contents')
uploaded_filename: str = APIField(..., example="firmware.bin")
class UploadFirmwareSchema(Schema):
project_name: str = APIField(..., example="c3nav_positioning")
version: str = APIField(..., example="499837d-dirty")
idf_version: str = APIField(..., example="v5.1-476-g3187b8b326")
2023-11-17 16:42:27 +01:00
builds: list[UploadFirmwareBuildSchema] = APIField(..., min_items=1, unique_items=True)
@validator('builds')
def builds_variants_must_be_unique(cls, builds):
if len(set(build.variant for build in builds)) != len(builds):
raise ValueError("builds must have unique variant identifiers")
return builds
@api_router.post('/firmwares/upload', summary="Upload firmware", auth=BearerAuth(superuser=True),
2023-11-17 16:42:27 +01:00
description="your OpenAPI viewer might not show it: firmware_data is UploadFirmwareSchema as json",
response={200: FirmwareSchema, **auth_permission_responses, **APIConflict.dict()})
def firmware_upload(request, firmware_data: UploadFirmwareSchema, binary_files: list[UploadedFile]):
binary_files_by_name = {binary_file.name: binary_file for binary_file in binary_files}
if len([binary_file.name for binary_file in binary_files]) != len(binary_files_by_name):
raise APIRequestValidationFailed("Filenames of uploaded binary files must be unique.")
build_filenames = [build_data.uploaded_filename for build_data in firmware_data.builds]
if len(build_filenames) != len(set(build_filenames)):
raise APIRequestValidationFailed("Builds need to refer to different unique binary file names.")
if set(binary_files_by_name) != set(build_filenames):
raise APIRequestValidationFailed("All uploaded binary files need to be refered to by one build.")
try:
with transaction.atomic():
version = FirmwareVersion.objects.create(
project_name=firmware_data.project_name,
version=firmware_data.version,
idf_version=firmware_data.idf_version,
uploader=request.auth,
)
for build_data in firmware_data.builds:
# if bin_file.size > 4 * 1024 * 1024:
# raise ValueError # todo: better error
# h = hashlib.sha256()
# h.update(build_data.binary)
# sha256_bin_file = h.hexdigest() # todo: verify sha256 correctly
#
# if sha256_bin_file != build_data.sha256_hash:
# raise ValueError
build = version.builds.create(
variant=build_data.variant,
chip=build_data.chip,
sha256_hash=build_data.sha256_hash,
project_description=build_data.project_description,
binary=binary_files_by_name[build_data.uploaded_filename],
)
for board in build_data.boards:
build.firmwarebuildboard_set.create(board=board)
except IntegrityError:
raise APIConflict('Firmware version already exists.')
return version