Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 24 additions & 0 deletions docker-app/qfieldcloud/core/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,27 @@ def _list_files(self, user: User, project: Project) -> HttpResponse | Response:
self.client.credentials(HTTP_AUTHORIZATION="")

return response

def _get_file_metadata(
self,
user: User,
project: Project,
filename: str,
) -> HttpResponse | Response:
token = self._get_token_for_user(user)

self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)

response = self.client.get(
reverse(
"filestorage_file_metadata",
kwargs={
"project_id": project.id,
"filename": filename,
},
)
)

self.client.credentials(HTTP_AUTHORIZATION="")

return response
61 changes: 61 additions & 0 deletions docker-app/qfieldcloud/core/views/files_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,67 @@ def get(self, request: Request, projectid: str) -> Response:
return Response(result_list)


@extend_schema_view(
get=extend_schema(
description="Get the project's file metadata",
responses={200: FileWithVersionsSerializer()},
),
)
class LegacyFileMetadataView(views.APIView):
permission_classes = [permissions.IsAuthenticated, ListFilesViewPermissions]

def get(self, request: Request, projectid: str, filename: str) -> Response:
try:
project = Project.objects.get(id=projectid)
except ObjectDoesNotExist:
raise NotFound(detail=projectid)

file_obj = get_project_file_with_versions(projectid, filename)

if not file_obj:
raise NotFound(detail=filename)

versions_data = []
file_data: dict = {}

for version in file_obj.versions:
last_modified = version.last_modified.strftime(
settings.QFIELDCLOUD_STORAGE_DT_LAST_MODIFIED_FORMAT
)
md5sum = version.md5sum

head = version._data.head()
# We cannot be sure of the metadata's first letter case
# https://github.com/boto/boto3/issues/1709
metadata = head["Metadata"]
sha256sum = metadata.get("sha256sum") or metadata.get("Sha256sum")

version_data = {
"size": version.size,
"md5sum": md5sum,
"version_id": version.id,
"last_modified": last_modified,
"is_latest": version.is_latest,
"display": version.display,
"sha256": sha256sum,
}

versions_data.append(version_data)

if version.is_latest:
is_attachment = get_attachment_dir_prefix(project, filename) != ""

file_data["name"] = filename
file_data["size"] = version.size
file_data["md5sum"] = md5sum
file_data["last_modified"] = last_modified
file_data["is_attachment"] = is_attachment
file_data["sha256"] = sha256sum

file_data["versions"] = versions_data
return Response(file_data)


class DownloadPushDeleteFileViewPermissions(permissions.BasePermission):
def has_permission(self, request, view):
if "projectid" not in request.parser_context["kwargs"]:
Expand Down
17 changes: 17 additions & 0 deletions docker-app/qfieldcloud/filestorage/tests/test_files_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,3 +1068,20 @@ def test_thumbnail_storage_key_is_variable(self):
self.assertNotEqual(thumbnail_key1, "thumbnail.png")
self.assertNotEqual(thumbnail_key2, "thumbnail2.svg")
self.assertNotEqual(thumbnail_key1, thumbnail_key2)

def test_get_file_metadata(self):
filename = "file.name"
content = "Hello!"
self.assertFileUploaded(self.u1, self.p1, filename, StringIO(content))

response = self._get_file_metadata(self.u1, self.p1, filename)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(data["name"], filename)
self.assertEqual(data["size"], len(content))
self.assertIn("sha256", data)
self.assertIn("md5sum", data)

# nonexistent file
response = self._get_file_metadata(self.u1, self.p1, "nonexistent.file")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
6 changes: 6 additions & 0 deletions docker-app/qfieldcloud/filestorage/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
AvatarFileReadView,
compatibility_file_crud_view,
compatibility_file_list_view,
compatibility_file_metadata_view,
compatibility_project_meta_file_read_view,
)

Expand Down Expand Up @@ -34,4 +35,9 @@
AvatarFileReadView.as_view(),
name="filestorage_named_avatars",
),
path(
"files/metadata/<uuid:project_id>/<path:filename>/",
compatibility_file_metadata_view,
name="filestorage_file_metadata",
),
]
58 changes: 58 additions & 0 deletions docker-app/qfieldcloud/filestorage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from qfieldcloud.core.views.files_views import (
DownloadPushDeleteFileView as LegacyFileCrudView,
)
from qfieldcloud.core.views.files_views import (
LegacyFileMetadataView,
)
from qfieldcloud.core.views.files_views import (
ListFilesView as LegacyFileListView,
)
Expand Down Expand Up @@ -126,6 +129,28 @@ def get_queryset(self, *args, **kwargs) -> QuerySet[File]: # type: ignore
return qs


@extend_schema_view(
get=extend_schema(
description="Get the project's file metadata",
responses={200: FileWithVersionsSerializer()},
),
)
class FileMetadataView(generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated, FileListViewPermissions]
serializer_class = FileWithVersionsSerializer
lookup_field = "name"
lookup_url_kwarg = "filename"

def get_queryset(self):
project_id = self.kwargs["project_id"]

return (
File.objects.select_related("project", "latest_version")
.prefetch_related("versions")
.filter(project_id=project_id, file_type=File.FileType.PROJECT_FILE)
)


class FileCrudView(views.APIView):
permission_classes = [permissions.IsAuthenticated, FileCrudViewPermissions]

Expand Down Expand Up @@ -279,6 +304,39 @@ def compatibility_file_list_view(
return FileListView.as_view(**view_kwargs)(request, *args, **kwargs)


@csrf_exempt
def compatibility_file_metadata_view(
request: Request, *args, **kwargs
) -> Response | HttpResponse:
"""
Todo:
* Delete with QF-4963 Drop support for legacy storage
"""
# let's assume that `kwargs["project_id"]` will no throw a `KeyError`
project_id: UUID = kwargs["project_id"]
view_kwargs = kwargs.pop("view_kwargs", {})

try:
project = Project.objects.get(id=project_id)
except Project.DoesNotExist:
# if the project does not exist, we just fallback to the new view, which will return JSON formatted 404 later
return FileMetadataView.as_view(**view_kwargs)(request, *args, **kwargs)

if project.uses_legacy_storage:
# rename the `project_id` to previously used `projectid`, so we don't change anything in the legacy code
kwargs["projectid"] = kwargs.pop("project_id")

logger.debug(f"Project {project_id=} will be using the legacy file management.")

return LegacyFileMetadataView.as_view(**view_kwargs)(request, *args, **kwargs)
else:
logger.debug(
f"Project {project_id=} will be using the regular file management."
)

return FileMetadataView.as_view(**view_kwargs)(request, *args, **kwargs)


@csrf_exempt
def compatibility_file_crud_view(
request: Request, *args, **kwargs
Expand Down
Loading