Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions gateway/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,16 @@ def __get_random_token(length: int) -> str:
"SDS_NEW_USERS_APPROVED_ON_CREATION",
default=False,
)

# File upload limits
# ------------------------------------------------------------------------------
# Maximum number of files that can be uploaded at once
DATA_UPLOAD_MAX_NUMBER_FILES: int = env.int(
"DATA_UPLOAD_MAX_NUMBER_FILES", default=1000
)

# Maximum memory size for file uploads (default: 2.5MB, increased to 100MB)
DATA_UPLOAD_MAX_MEMORY_SIZE: int = env.int(
"DATA_UPLOAD_MAX_MEMORY_SIZE",
default=104857600, # 100MB
)
146 changes: 146 additions & 0 deletions gateway/sds_gateway/api_methods/helpers/file_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from http import HTTPStatus

from rest_framework import status
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory

from sds_gateway.api_methods.views.capture_endpoints import CaptureViewSet
from sds_gateway.api_methods.views.file_endpoints import CheckFileContentsExistView
from sds_gateway.api_methods.views.file_endpoints import FileViewSet


def upload_file_helper_simple(request, file_data):
"""Upload a single file using FileViewSet.create.

file_data should contain all required fields: name, directory, file,
media_type, etc. Returns ([response], []) for success, ([], [error]) for
error, and handles 409 as a warning.
"""
factory = APIRequestFactory()
django_request = factory.post(
request.path,
file_data,
format="multipart",
)
django_request.user = request.user
drf_request = Request(django_request, parsers=[MultiPartParser()])
drf_request.user = request.user
view = FileViewSet()
view.request = drf_request
view.action = "create"
view.format_kwarg = None
view.args = ()
view.kwargs = {}
try:
response = view.create(drf_request)
except (ValueError, TypeError, AttributeError, KeyError) as e:
return [], [f"Data validation error: {e}"]
else:
responses = []
errors = []

if not hasattr(response, "status_code"):
errors.append(getattr(response, "data", str(response)))
else:
http_status = HTTPStatus(response.status_code)
response_data = getattr(response, "data", str(response))

if http_status.is_success:
responses.append(response)
elif response.status_code == status.HTTP_409_CONFLICT:
# Already exists, treat as warning
errors.append(response_data)
elif http_status.is_server_error:
# Handle 500 and other server errors
errors.append("Internal server error")
elif http_status.is_client_error:
# Handle 4xx client errors
errors.append(f"Client error ({response.status_code}): {response_data}")
else:
# Handle any other status codes
errors.append(response_data)

return responses, errors


# TODO: Use this helper method when implementing the file upload mode multiplexer.
def check_file_contents_exist_helper(request, check_data):
"""Call the post method of CheckFileContentsExistView with the given data.

check_data should contain the required fields: directory, name, sum_blake3,
etc.
"""
factory = APIRequestFactory()
django_request = factory.post(
request.path, # or a specific path for the check endpoint
check_data,
format="multipart",
)
django_request.user = request.user
drf_request = Request(django_request, parsers=[MultiPartParser()])
drf_request.user = request.user
view = CheckFileContentsExistView()
view.request = drf_request
view.action = None
view.format_kwarg = None
view.args = ()
view.kwargs = {}
return view.post(drf_request)


def create_capture_helper_simple(request, capture_data):
"""Create a capture using CaptureViewSet.create.

capture_data should contain all required fields for capture creation:
owner, top_level_dir, capture_type, channel, index_name, etc.
Returns ([response], []) for success, ([], [error]) for error, and handles
409 as a warning.
"""
factory = APIRequestFactory()
django_request = factory.post(
request.path,
capture_data,
format="multipart",
)
django_request.user = request.user
drf_request = Request(django_request, parsers=[MultiPartParser()])
drf_request.user = request.user
view = CaptureViewSet()
view.request = drf_request
view.action = "create"
view.format_kwarg = None
view.args = ()
view.kwargs = {}
# Set the context for the serializer
view.get_serializer_context = lambda: {"request_user": request.user}
try:
response = view.create(drf_request)
except (ValueError, TypeError, AttributeError, KeyError) as e:
return [], [f"Data validation error: {e}"]
else:
responses = []
errors = []

if not hasattr(response, "status_code"):
errors.append(getattr(response, "data", str(response)))
else:
http_status = HTTPStatus(response.status_code)
response_data = getattr(response, "data", str(response))

if http_status.is_success:
responses.append(response)
elif response.status_code == status.HTTP_409_CONFLICT:
# Already exists, treat as warning
errors.append(response_data)
elif http_status.is_server_error:
# Handle 500 and other server errors
errors.append(f"Server error ({response.status_code}): {response_data}")
elif http_status.is_client_error:
# Handle 4xx client errors
errors.append(f"Client error ({response.status_code}): {response_data}")
else:
# Handle any other status codes
errors.append(response_data)

return responses, errors
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ def check_file_contents_exist(
user=user,
)

log.debug(f"Checking file contents for user in directory: {safe_dir}")
identical_file: File | None = identical_user_owned_file.filter(
directory=safe_dir,
name=name,
Expand All @@ -240,14 +239,12 @@ def check_file_contents_exist(
user_mutable_attributes_differ = True
break

payload = {
return {
"file_exists_in_tree": identical_file is not None,
"file_contents_exist_for_user": file_contents_exist_for_user,
"user_mutable_attributes_differ": user_mutable_attributes_differ,
"asset_id": asset.uuid if asset else None,
}
log.debug(payload)
return payload


class FileCheckResponseSerializer(serializers.Serializer[File]):
Expand Down
34 changes: 24 additions & 10 deletions gateway/sds_gateway/api_methods/utils/metadata_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
# the mapping below is used for drf capture metadata parsing in extract_drf_metadata.py

import logging
from typing import TYPE_CHECKING
from typing import Any

from sds_gateway.api_methods.models import CaptureType
if TYPE_CHECKING:
from sds_gateway.api_methods.models import CaptureType

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -357,10 +359,8 @@
"capture_props",
]

capture_index_mapping_by_type: dict[CaptureType, dict[str, dict[str, Any]]] = {
CaptureType.DigitalRF: drf_capture_index_mapping,
CaptureType.RadioHound: rh_capture_index_mapping,
}
# This will be populated at runtime to avoid circular imports
capture_index_mapping_by_type = {}

base_properties = {
"channel": {"type": "keyword"},
Expand All @@ -387,9 +387,20 @@


def get_mapping_by_capture_type(
capture_type: CaptureType,
capture_type: Any,
) -> dict[str, str | dict[str, Any]]:
"""Get the mapping for a given capture type."""
# Local import to avoid circular dependency
from sds_gateway.api_methods.models import CaptureType

# Initialize mapping if not already done
if not capture_index_mapping_by_type:
capture_index_mapping_by_type.update(
{
CaptureType.DigitalRF: drf_capture_index_mapping,
CaptureType.RadioHound: rh_capture_index_mapping,
}
)

return {
"properties": {
Expand All @@ -406,14 +417,17 @@ def get_mapping_by_capture_type(
}


def infer_index_name(capture_type: CaptureType) -> str:
def infer_index_name(capture_type: "CaptureType") -> str:
"""Infer the index name for a given capture."""
# Populate index_name based on capture type
# Local import to avoid circular dependency
from sds_gateway.api_methods.models import CaptureType

# Handle enum inputs (strings match fine against StrEnum)
match capture_type:
case CaptureType.DigitalRF:
return f"captures-{CaptureType.DigitalRF}"
return f"captures-{capture_type.value}"
case CaptureType.RadioHound:
return f"captures-{CaptureType.RadioHound}"
return f"captures-{capture_type.value}"
case _:
msg = f"Invalid capture type: {capture_type}"
log.error(msg)
Expand Down
Loading