wip: add v2 upload endpoint

2274-implement-new-upload-api
Georg Krause 2024-01-29 13:48:22 +00:00
rodzic d3879e4792
commit b2866adfe3
5 zmienionych plików z 192 dodań i 6 usunięć

Wyświetl plik

@ -1456,3 +1456,7 @@ class UploadGroup(models.Model):
def __str__(self):
return self.name
@property
def upload_url(self):
return f"{settings.FUNKWHALE_URL}/api/v2/upload-groups/{self.guid}/uploads"

Wyświetl plik

@ -858,8 +858,99 @@ class UploadGroupSerializer(serializers.ModelSerializer):
fields = ["guid", "name", "createdAt", "uploadUrl"]
name = serializers.CharField(required=False)
uploadUrl = serializers.SerializerMethodField(read_only=True)
uploadUrl = serializers.URLField(read_only=True, source="upload_url")
createdAt = serializers.DateTimeField(read_only=True, source="created_at")
def get_uploadUrl(self, value):
return f"{settings.FUNKWHALE_URL}/api/v2/upload-groups/{value.guid}/uploads"
class UploadGroupUploadMetadataReleaseSerializer(serializers.Serializer):
title = serializers.CharField()
artist = serializers.CharField()
mbid = serializers.UUIDField(required=False)
class UploadGroupUploadMetadataArtistSerializer(serializers.Serializer):
name = serializers.CharField()
mbid = serializers.UUIDField(required=False)
class UploadGroupUploadMetadataSerializer(serializers.Serializer):
title = serializers.CharField()
mbid = serializers.UUIDField(required=False)
tags = serializers.ListField(child=serializers.CharField(), required=False)
position = serializers.IntegerField(required=False)
entryNumber = serializers.IntegerField(required=False)
releaseDate = serializers.DateField(required=False)
license = serializers.URLField(required=False)
release = UploadGroupUploadMetadataReleaseSerializer(required=False)
artist = UploadGroupUploadMetadataArtistSerializer(required=False)
class TargetSerializer(serializers.Serializer):
library = serializers.UUIDField(required=False)
collections = serializers.ListField(child=serializers.UUIDField(), required=False)
channels = serializers.ListField(child=serializers.UUIDField(), required=False)
def validate(self, data):
# At the moment we allow to set exactly one target, it can be either a library or a channel.
# The structure already allows setting multiple targets in the future, however this is disabled for now.
if "channels" in data and "library" in data:
raise serializers.ValidationError
if "channels" not in data and "library" not in data:
raise serializers.ValidationError
if "collections" in data:
raise serializers.ValidationError("Not yet implemented")
try:
if len(data.channels) > 1:
raise serializers.ValidationError
except AttributeError:
pass
return data
class UploadGroupUploadSerializer(serializers.ModelSerializer):
class Meta:
model = models.Upload
fields = [
"audioFile",
"target",
"metadata",
] # , "cover"] TODO we need to process the cover
metadata = serializers.JSONField(source="import_metadata")
target = serializers.JSONField()
audioFile = serializers.FileField(source="audio_file")
# cover = serializers.FileField(required=False)
def validate_target(self, value):
serializer = TargetSerializer(data=value)
if serializer.is_valid():
return serializer.validated_data
else:
print(serializer.errors)
raise serializers.ValidationError
def validate_metadata(self, value):
serializer = UploadGroupUploadMetadataSerializer(data=value)
if serializer.is_valid():
return serializer.validated_data
else:
print(serializer.errors)
raise serializers.ValidationError
def create(self, validated_data):
library = models.Library.objects.get(uuid=validated_data["target"]["library"])
del validated_data["target"]
return models.Upload.objects.create(
library=library, source="upload://test", **validated_data
)
class BaseUploadSerializer(serializers.ModelSerializer):
class Meta:
model = models.Upload
fields = ["guid", "createdDate", "uploadGroup", "status"]
guid = serializers.UUIDField(source="uuid")
createdDate = serializers.DateTimeField(source="creation_date")
uploadGroup = serializers.UUIDField(source="upload_group.guid")
status = serializers.CharField(source="import_status")

Wyświetl plik

@ -15,6 +15,7 @@ from rest_framework import mixins, renderers
from rest_framework import settings as rest_settings
from rest_framework import views, viewsets
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from funkwhale_api.common import decorators as common_decorators
@ -946,3 +947,26 @@ class UploadGroupViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(owner=self.request.user.actor)
@action(
detail=True,
methods=["post"],
parser_classes=(MultiPartParser, FormParser),
serializer_class=serializers.UploadGroupUploadSerializer,
)
def uploads(self, request, pk=None):
print(request.data)
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
upload_group = models.UploadGroup.objects.get(guid=pk)
if upload_group.owner == request.user.actor:
upload = serializer.save(upload_group=upload_group)
common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)
response = serializers.BaseUploadSerializer(upload).data
return Response(response, status=200)
else:
return Response("You don't own this Upload Group", status=403)
else:
print(serializer.errors)
return Response("Fehler", status=202)

Wyświetl plik

@ -1,6 +1,18 @@
import pytest
from django.urls import reverse
def test_can_resolve_upload_urls():
url = reverse("api:v2:upload-groups-list")
assert url == "/api/v2/upload-groups"
@pytest.mark.parametrize(
"input,args,expected_url",
[
("api:v2:upload-groups-list", None, "/api/v2/upload-groups"),
(
"api:v2:upload-groups-uploads",
["1234-1234-1234"],
"/api/v2/upload-groups/1234-1234-1234/uploads",
),
],
)
def test_can_resolve_upload_urls(input, args, expected_url):
url = reverse(input, args=args)
assert url == expected_url

Wyświetl plik

@ -1672,3 +1672,58 @@ def test_can_create_upload_group_with_name(logged_in_api_client):
assert "https://test.federation/api/v2/upload-groups/" in response.data.get(
"uploadUrl"
)
def test_user_can_create_upload_v2(logged_in_api_client, factories, mocker, audio_file):
library = factories["music.Library"](actor__user=logged_in_api_client.user)
logged_in_api_client.user.create_actor()
upload_group = factories["music.UploadGroup"](owner=logged_in_api_client.user.actor)
upload_url = upload_group.upload_url
m = mocker.patch("funkwhale_api.common.utils.on_commit")
response = logged_in_api_client.post(
upload_url,
{
"audioFile": audio_file,
"metadata": '{"title": "foo"}',
"target": f'{{"library": "{ library.uuid }"}}',
},
)
print(response.data)
assert response.status_code == 200
upload = library.uploads.latest("id")
audio_file.seek(0)
assert upload.audio_file.read() == audio_file.read()
assert upload.source == "upload://test"
assert upload.import_status == "pending"
assert upload.import_metadata == {"title": "foo"}
assert upload.track is None
assert upload.upload_group == upload_group
m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk)
def test_user_cannot_create_upload_for_foreign_group(
logged_in_api_client, factories, mocker, audio_file
):
library = factories["music.Library"](actor__user=logged_in_api_client.user)
logged_in_api_client.user.create_actor()
upload_group = factories["music.UploadGroup"]()
upload_url = upload_group.upload_url
response = logged_in_api_client.post(
upload_url,
{
"audioFile": audio_file,
"metadata": '{"title": "foo"}',
"target": f'{{"library": "{ library.uuid }"}}',
},
)
assert response.status_code == 403