diff --git a/geonode/assets/admin.py b/geonode/assets/admin.py new file mode 100644 index 00000000000..e78715de724 --- /dev/null +++ b/geonode/assets/admin.py @@ -0,0 +1,55 @@ +import json +import logging +from django.db import models +from django.forms import widgets +from django.contrib import admin + +from geonode.assets.local import LocalAssetHandler +from geonode.assets.models import LocalAsset +from geonode.base.models import Link + +logger = logging.getLogger(__name__) + + +class PrettyJSONWidget(widgets.Textarea): + + def format_value(self, value): + try: + value = json.dumps(json.loads(value), indent=2, sort_keys=True) + # these lines will try to adjust size of TextArea to fit to content + row_lengths = [len(r) for r in value.split("\n")] + self.attrs["rows"] = min(max(len(row_lengths) + 2, 10), 30) + self.attrs["cols"] = min(max(max(row_lengths) + 2, 40), 120) + return value + except Exception as e: + logger.warning("Error while formatting JSON: {}".format(e)) + return super(PrettyJSONWidget, self).format_value(value) + + +@admin.register(LocalAsset) +class LocalAssetAdmin(admin.ModelAdmin): + model = LocalAsset + + list_display = ("id", "title", "type", "owner", "created_formatted", "managed", "links", "link0") + list_display_links = ("id", "title") + + formfield_overrides = {models.JSONField: {"widget": PrettyJSONWidget}} + + def created_formatted(self, obj): + return obj.created.strftime("%Y-%m-%d %H:%M:%S") + + def links(self, obj): + return Link.objects.filter(asset=obj).count() + + def link0(self, obj): + link = Link.objects.filter(asset=obj).first() + return f"{link.link_type} {link.extension}: {link.name}" if link else None + + def managed(self, obj) -> bool: + try: + return LocalAssetHandler._is_file_managed(obj.location[0]) + except Exception as e: + logger.error("Bad location for asset obj: {e}") + return None + + managed.boolean = True diff --git a/geonode/assets/local.py b/geonode/assets/local.py index 2c557a57161..889c2971576 100644 --- a/geonode/assets/local.py +++ b/geonode/assets/local.py @@ -102,11 +102,13 @@ def create_download_url(self, asset) -> str: def create_link_url(self, asset) -> str: return build_absolute_uri(reverse("assets-link", args=(asset.pk,))) - def _is_file_managed(self, file) -> bool: + @classmethod + def _is_file_managed(cls, file) -> bool: assets_root = os.path.normpath(settings.ASSETS_ROOT) return file.startswith(assets_root) - def _are_files_managed(self, files: list) -> bool: + @classmethod + def _are_files_managed(cls, files: list) -> bool: """ :param files: files to be checked :return: True if all files are managed, False is no file is managed @@ -114,7 +116,7 @@ def _are_files_managed(self, files: list) -> bool: """ managed = unmanaged = None for file in files: - if self._is_file_managed(file): + if cls._is_file_managed(file): managed = True else: unmanaged = True @@ -127,7 +129,9 @@ def _are_files_managed(self, files: list) -> bool: class LocalAssetDownloadHandler(AssetDownloadHandlerInterface): - def create_response(self, asset: LocalAsset, attachment: bool = False, basename=None) -> HttpResponse: + def create_response( + self, asset: LocalAsset, attachment: bool = False, basename: str = None, path: str = None + ) -> HttpResponse: if not asset.location: return HttpResponse("Asset does not contain any data", status=500) @@ -135,21 +139,42 @@ def create_response(self, asset: LocalAsset, attachment: bool = False, basename= logger.warning("TODO: Asset contains more than one file. Download needs to be implemented") file0 = asset.location[0] - filename = os.path.basename(file0) - orig_base, ext = os.path.splitext(filename) - outname = f"{basename or orig_base}{ext}" + if not path: # use the file definition + if not os.path.isfile(file0): + logger.warning(f"Default file {file0} not found for asset {asset.id}") + return HttpResponse(f"Default file not found for asset {asset.id}", status=400) + localfile = file0 + + else: # a specific file is requested + if "/../" in path: # we may want to improve fraudolent request detection + logger.warning(f"Tentative path traversal for asset {asset.id}") + return HttpResponse(f"File not found for asset {asset.id}", status=400) + + if os.path.isfile(file0): + dir0 = os.path.dirname(file0) + elif os.path.isdir(file0): + dir0 = file0 + else: + return HttpResponse(f"Unexpected internal location '{file0}' for asset {asset.id}", status=500) + + localfile = os.path.join(dir0, path) + logger.debug(f"Requested path {dir0} + {path}") + + if os.path.isfile(localfile): + filename = os.path.basename(localfile) + orig_base, ext = os.path.splitext(filename) + outname = f"{basename or orig_base or 'file'}{ext}" - if _asset_storage_manager.exists(file0): - logger.info(f"Returning file {file0} with name {outname}") + logger.info(f"Returning file '{localfile}' with name '{outname}'") return DownloadResponse( - _asset_storage_manager.open(file0).file, + _asset_storage_manager.open(localfile).file, basename=f"{outname}", attachment=attachment, ) else: - logger.warning(f"Internal file {file0} not found for asset {asset.id}") - return HttpResponse(f"Internal file not found for asset {asset.id}", status=500) + logger.warning(f"Internal file {localfile} not found for asset {asset.id}") + return HttpResponse(f"Internal file not found for asset {asset.id}", status=404 if path else 500) asset_handler_registry.register(LocalAssetHandler) diff --git a/geonode/assets/models.py b/geonode/assets/models.py index fe360258209..a4fe3a26de6 100644 --- a/geonode/assets/models.py +++ b/geonode/assets/models.py @@ -36,7 +36,7 @@ class Meta: verbose_name_plural = "Local assets" def __str__(self) -> str: - return super().__str__() + return f"{self.__class__.__name__}: {self.type}|{self.title}" def cleanup_asset_data(instance, *args, **kwargs): diff --git a/geonode/assets/views.py b/geonode/assets/views.py index b5a5990c99d..93435d2743f 100644 --- a/geonode/assets/views.py +++ b/geonode/assets/views.py @@ -85,28 +85,29 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def _get_file(self, request, pk, attachment: bool): + def _get_file(self, request, pk, attachment: bool = False, path=None): asset = get_object_or_404(Asset, pk=pk) if bad_response := get_perms_response(request, asset): return bad_response asset_handler = asset_handler_registry.get_handler(asset) # TODO: register_event(request, EventType.EVENT_DOWNLOAD, asset) - return asset_handler.get_download_handler(asset).create_response(asset, attachment) + return asset_handler.get_download_handler(asset).create_response(asset, path=path, attachment=attachment) @action( detail=False, - url_path="(?P\d+)/download", # noqa + url_path="(?P\d+)/download(/(?P.*))?", # noqa # url_name="asset-download", methods=["get"], ) - def download(self, request, pk=None, *args, **kwargs): - return self._get_file(request, pk, True) + def download(self, request, pk=None, path=None, *args, **kwargs): + return self._get_file(request, pk, attachment=True, path=path) @action( detail=False, - url_path="(?P\d+)/link", # noqa + url_path="(?P\d+)/link(/(?P.*))?", # noqa # url_name="asset-link", methods=["get"], ) - def link(self, request, pk=None, *args, **kwargs): - return self._get_file(request, pk, False) + def link(self, request, pk=None, path=None, *args, **kwargs): + logger.warning(f"REQUESTED ASSET LINK FOR PK:{pk} PATH:{path}") + return self._get_file(request, pk, attachment=False, path=path)