Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ca857dd
upload captures work in progress
LLKruczek Jul 14, 2025
2284806
updated result display modal. Still missing capture error handling.
LLKruczek Jul 14, 2025
885f163
trying to link files to newly created capture
LLKruczek Jul 14, 2025
da0c675
updated file and capture creation with serializer calls
LLKruczek Jul 16, 2025
34f2fb0
updated file_list.html
LLKruczek Jul 16, 2025
96bd720
initial commit
LLKruczek Jul 22, 2025
2db357f
Discard changes to .pre-commit-config.yaml
lucaspar Jul 22, 2025
c21dd2b
moved upload capture button
LLKruczek Jul 22, 2025
8e10578
Remove unnecessary file
LLKruczek Jul 22, 2025
5eaa89c
Updates for the first round of comments
LLKruczek Jul 22, 2025
0774171
implemented capture creation. Fixed comments
LLKruczek Jul 22, 2025
272f9a6
added digitalrf and radiohound selection. Have not tested radiohound …
LLKruczek Jul 28, 2025
cb005c7
added digitalrf and radiohound selection. Starting to work on CheckFi…
LLKruczek Jul 28, 2025
eb6944a
Add Django migrations and pre-commit cache to .gitignore
LLKruczek Jul 30, 2025
4c80a5c
Implemented rh capture creation
LLKruczek Jul 30, 2025
3b05884
style changes. Enabled RH and DigitalRF capture creation and file upl…
LLKruczek Aug 4, 2025
81124c9
Implemented chunked uploads and disconnection/cancellation handling
LLKruczek Aug 5, 2025
7f4d83b
Fixed reloading page conflict
LLKruczek Aug 5, 2025
2f956b6
Got rid of debugging messages
LLKruczek Aug 5, 2025
d542bab
Fixed display message on the upload modal
LLKruczek Aug 5, 2025
cff529d
fixed comments
LLKruczek Aug 6, 2025
5eaa5e6
fixed comments
LLKruczek Aug 6, 2025
a1f2621
fixed comments
LLKruczek Aug 6, 2025
f5f9238
Removed disconnection detection codes that are no longer used
LLKruczek Aug 6, 2025
d6cabef
Removed TODO comments
LLKruczek Aug 6, 2025
be14ef6
restore using enum
LLKruczek Aug 7, 2025
ef067b6
fixed comments
LLKruczek Aug 7, 2025
6b468c9
improved error handling and pre-validation
LLKruczek Aug 8, 2025
5139a8a
refactored capture creation code
LLKruczek Aug 13, 2025
1f1d320
Added type hint
LLKruczek Aug 13, 2025
39533f1
simplified file upload status to success or error
LLKruczek Aug 14, 2025
57e8b85
Enhanced error handling
LLKruczek Aug 14, 2025
9fb37d1
got rid of determine_status_code
LLKruczek Aug 15, 2025
5d1b60a
changed file count return
LLKruczek Aug 15, 2025
043ae34
simplified returning UUIDs of created captures
LLKruczek Aug 15, 2025
bfd5b8c
fixed comments
LLKruczek Aug 15, 2025
cdbb31c
file manager show channel
srucker01 Aug 15, 2025
2520267
fixed capture search
srucker01 Aug 15, 2025
6395a71
Add file upload functionality - clean merge ready
srucker01 Aug 19, 2025
4f15ed6
added error catching to the backend for better error message display.…
LLKruczek Aug 19, 2025
e77c4f8
add a file to a dataset
srucker01 Aug 19, 2025
71850eb
imports
srucker01 Aug 19, 2025
50d3b16
css updates
srucker01 Aug 19, 2025
9157dd1
remove js from template
srucker01 Aug 19, 2025
afed8e3
drag and drop
srucker01 Aug 19, 2025
5527fd0
Merge branch 'upload-captures' into srucker_file_upload
srucker01 Aug 19, 2025
6ec4d37
removed dataset
srucker01 Aug 20, 2025
4e5fad6
file size check
srucker01 Aug 20, 2025
8f852b8
clear console
srucker01 Aug 20, 2025
d07650d
Merge branch 'master' into srucker_file_upload
srucker01 Aug 20, 2025
8d96e91
empty file
srucker01 Aug 20, 2025
98c419e
code duplication and clean up
srucker01 Aug 20, 2025
4c3e6db
error handling
srucker01 Aug 20, 2025
0dd997a
git comments
srucker01 Aug 28, 2025
fd542c1
Merge branch 'master' into srucker_file_upload
srucker01 Aug 28, 2025
fbc36a2
pre-commit changes
srucker01 Aug 28, 2025
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 @@ -502,3 +502,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
8 changes: 8 additions & 0 deletions gateway/sds_gateway/api_methods/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from django.dispatch import receiver
from django_cog.models import Pipeline

from .utils.metadata_schemas import infer_index_name
from .utils.opensearch_client import get_opensearch_client

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -285,6 +286,13 @@ def save(self, *args, **kwargs):
if not self.name and self.top_level_dir:
# Extract the last part of the path as the default name
self.name = Path(self.top_level_dir).name or self.top_level_dir.strip("/")

# Set the index_name if not provided
if not self.index_name and self.capture_type:
# Convert string to CaptureType enum
capture_type_enum = CaptureType(self.capture_type)
self.index_name = infer_index_name(capture_type_enum)

super().save(*args, **kwargs)

@property
Expand Down
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
35 changes: 25 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,9 @@
"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,
}
# Initialize empty dictionary to avoid circular import issues
# Will be populated dynamically in get_mapping_for_capture_type()
capture_index_mapping_by_type: dict[Any, dict[str, dict[str, Any]]] = {}

base_properties = {
"channel": {"type": "keyword"},
Expand All @@ -387,9 +388,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 # noqa: PLC0415

# 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 +418,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 # noqa: PLC0415

# 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