Skip to content

Commit

Permalink
[Fixes #12226] Directory assets
Browse files Browse the repository at this point in the history
  • Loading branch information
etj committed May 14, 2024
1 parent 8e9ed53 commit f2fbc1c
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 21 deletions.
55 changes: 55 additions & 0 deletions geonode/assets/admin.py
Original file line number Diff line number Diff line change
@@ -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
49 changes: 37 additions & 12 deletions geonode/assets/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,21 @@ 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
:raise: ValueError if both managed and unmanaged files are in the list
"""
managed = unmanaged = None
for file in files:
if self._is_file_managed(file):
if cls._is_file_managed(file):
managed = True
else:
unmanaged = True
Expand All @@ -127,29 +129,52 @@ 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)

if len(asset.location) > 1:
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)
2 changes: 1 addition & 1 deletion geonode/assets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
17 changes: 9 additions & 8 deletions geonode/assets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<pk>\d+)/download", # noqa
url_path="(?P<pk>\d+)/download(/(?P<path>.*))?", # 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<pk>\d+)/link", # noqa
url_path="(?P<pk>\d+)/link(/(?P<path>.*))?", # 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)

0 comments on commit f2fbc1c

Please sign in to comment.