Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fixes #12124] GNIP 100: Assets #12179

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
535d580
[Fixes #12124] GNIP 100: Assets
etj Apr 22, 2024
03c968d
fix test
mattiagiupponi Apr 29, 2024
32327a9
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 29, 2024
a6cf9f9
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 29, 2024
aa13c5f
Merge branch 'master' of github.com:GeoNode/geonode into 12124_assets
mattiagiupponi Apr 30, 2024
7737447
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 30, 2024
47c6c13
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 30, 2024
80311a5
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 30, 2024
1ec7613
[Fixes #12124] Fix broken tests
mattiagiupponi Apr 30, 2024
9d1697b
[Fixes #12124] GNIP 100: Assets - improve model
etj May 6, 2024
e7c8519
[Fixes #12124] GNIP 100: Assets - fix resource manager field list
etj May 7, 2024
c1028fb
[Fixes #12124] GNIP 100: Assets - minor test "in" fix
etj May 7, 2024
bdfa14e
Test removing handlerinfo
mattiagiupponi May 7, 2024
89722cb
Merge branch '12124_assets' of github.com:GeoNode/geonode into 12124_…
mattiagiupponi May 7, 2024
bb03c9e
Test add logger
mattiagiupponi May 7, 2024
1d023cc
#12124 Remove keepdb from test.sh
etj May 7, 2024
bb6c54e
#12124 Remove keepdb from test.sh
etj May 7, 2024
1aa69fc
Fix test
mattiagiupponi May 7, 2024
c50a11c
Fix format
mattiagiupponi May 7, 2024
5806872
[Fixes #12124] GNIP 100: Assets - improvements in clone and delete
etj May 7, 2024
291f93e
[Fixes #12124] GNIP 100: Assets - add tests and (un)managed files
etj May 9, 2024
64b4dee
[Fixes #12124] GNIP 100: Assets - tests
etj May 9, 2024
1e3b401
[Fixes #12124] GNIP 100: Assets - improve file handling
etj May 9, 2024
da90a2a
[Fixes #12124] GNIP 100: Assets - cleanup tmp doc file
etj May 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ SECRET_KEY='{secret_key}'

STATIC_ROOT=/mnt/volumes/statics/static/
MEDIA_ROOT=/mnt/volumes/statics/uploaded/
ASSETS_ROOT=/mnt/volumes/statics/assets/
GEOIP_PATH=/mnt/volumes/statics/geoip.db

CACHE_BUSTING_STATIC_ENABLED=False
Expand Down
Empty file added geonode/assets/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions geonode/assets/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#########################################################################
#
# Copyright (C) 2016 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
from django.apps import AppConfig

from geonode.notifications_helper import NotificationsAppConfigBase


class BaseAppConfig(NotificationsAppConfigBase, AppConfig):
name = "geonode.assets"

def ready(self):
super().ready()
run_setup_hooks()


def run_setup_hooks(*args, **kwargs):
from geonode.assets.handlers import asset_handler_registry

asset_handler_registry.init_registry()
91 changes: 91 additions & 0 deletions geonode/assets/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import logging

from django.conf import settings
from django.http import HttpResponse
from django.utils.module_loading import import_string

from geonode.assets.models import Asset

logger = logging.getLogger(__name__)


class AssetHandlerInterface:

def handled_asset_class(self):
raise NotImplementedError()

Check warning on line 15 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L15

Added line #L15 was not covered by tests

def create(self, title, description, type, owner, *args, **kwargs):
raise NotImplementedError()

Check warning on line 18 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L18

Added line #L18 was not covered by tests

def remove_data(self, asset: Asset, **kwargs):
raise NotImplementedError()

Check warning on line 21 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L21

Added line #L21 was not covered by tests

def replace_data(self, asset: Asset, files: list):
raise NotImplementedError()

Check warning on line 24 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L24

Added line #L24 was not covered by tests

def clone(self, asset: Asset) -> Asset:
"""
Creates a copy in the DB and copies the underlying data as well
"""
raise NotImplementedError()

Check warning on line 30 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L30

Added line #L30 was not covered by tests

def create_link_url(self, asset: Asset) -> str:
raise NotImplementedError()

Check warning on line 33 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L33

Added line #L33 was not covered by tests

def get_download_handler(self, asset: Asset):
raise NotImplementedError()

Check warning on line 36 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L36

Added line #L36 was not covered by tests

def get_storage_manager(self, asset):
raise NotImplementedError()

Check warning on line 39 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L39

Added line #L39 was not covered by tests


class AssetDownloadHandlerInterface:

def create_response(self, asset: Asset, attachment: bool = False, basename=None) -> HttpResponse:
raise NotImplementedError()

Check warning on line 45 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L45

Added line #L45 was not covered by tests


class AssetHandlerRegistry:
_registry = {}
_default_handler = None

def init_registry(self):
self.register_asset_handlers()
self.set_default_handler()

def register_asset_handlers(self):
for module_path in settings.ASSET_HANDLERS:
handler = import_string(module_path)
self.register(handler)
logger.info(f"Registered Asset handlers: {', '.join(settings.ASSET_HANDLERS)}")

def set_default_handler(self):
# check if declared class is registered
for handler in self._registry.values():
if ".".join([handler.__class__.__module__, handler.__class__.__name__]) == settings.DEFAULT_ASSET_HANDLER:
self._default_handler = handler
break

if self._default_handler is None:
logger.error(f"Could not set default asset handler class {settings.DEFAULT_ASSET_HANDLER}")

Check warning on line 70 in geonode/assets/handlers.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/handlers.py#L70

Added line #L70 was not covered by tests
else:
logger.info(f"Default Asset handler {settings.DEFAULT_ASSET_HANDLER}")

def register(self, asset_handler_class):
self._registry[asset_handler_class.handled_asset_class()] = asset_handler_class()

def get_default_handler(self) -> AssetHandlerInterface:
return self._default_handler

def get_handler(self, asset):
asset_cls = asset if isinstance(asset, type) else asset.__class__
ret = self._registry.get(asset_cls, None)
if not ret:
logger.warning(f"Could not find asset handler for {asset_cls}::{asset.__class__}")
logger.warning("Available asset types:")
for k, v in self._registry.items():
logger.warning(f"{k} --> {v.__class__.__name__}")
return ret


asset_handler_registry = AssetHandlerRegistry()
155 changes: 155 additions & 0 deletions geonode/assets/local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import datetime
import logging
import os

from django.conf import settings
from django.http import HttpResponse
from django.urls import reverse
from django_downloadview import DownloadResponse

from geonode.assets.handlers import asset_handler_registry, AssetHandlerInterface, AssetDownloadHandlerInterface
from geonode.assets.models import LocalAsset
from geonode.storage.manager import DefaultStorageManager, StorageManager
from geonode.utils import build_absolute_uri


logger = logging.getLogger(__name__)

_asset_storage_manager = StorageManager(
concrete_storage_manager=DefaultStorageManager(location=os.path.dirname(settings.ASSETS_ROOT))
)


class LocalAssetHandler(AssetHandlerInterface):
@staticmethod
def handled_asset_class():
return LocalAsset

def get_download_handler(self, asset):
return LocalAssetDownloadHandler()

def get_storage_manager(self, asset):
return _asset_storage_manager

def create(self, title, description, type, owner, files=None, clone_files=False, *args, **kwargs):
if not files:
raise ValueError("File(s) expected")

Check warning on line 36 in geonode/assets/local.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/local.py#L36

Added line #L36 was not covered by tests

if clone_files:
prefix = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
files = _asset_storage_manager.copy_files_list(files, dir=settings.ASSETS_ROOT, dir_prefix=prefix)
# TODO: please note the copy_files_list will make flat any directory structure

asset = LocalAsset(
title=title,
description=description,
type=type,
owner=owner,
created=datetime.datetime.now(),
location=files,
)
asset.save()
return asset

def remove_data(self, asset: LocalAsset):
"""
Removes the files related to an Asset.
Only files within the Assets directory are removed
"""
removed_dir = set()
for file in asset.location:
is_managed = self._is_file_managed(file)
if is_managed:
logger.info(f"Removing asset file {file}")
_asset_storage_manager.delete(file)
removed_dir.add(os.path.dirname(file))
else:
logger.info(f"Not removing asset file outside asset directory {file}")

# TODO: in case of subdirs, make sure that all the tree is removed in the proper order
for dir in removed_dir:
if not os.path.exists(dir):
logger.warning(f"Trying to remove not existing asset directory {dir}")
continue
if not os.listdir(dir):
logger.info(f"Removing empty asset directory {dir}")
os.rmdir(dir)

def replace_data(self, asset: LocalAsset, files: list):
self.remove_data(asset)
asset.location = files
asset.save()

def clone(self, source: LocalAsset) -> LocalAsset:
# get a new asset instance to be edited and stored back
asset = LocalAsset.objects.get(pk=source.pk)
# only copy files if they are managed
if self._are_files_managed(asset.location):
asset.location = _asset_storage_manager.copy_files_list(
asset.location, dir=settings.ASSETS_ROOT, dir_prefix=datetime.datetime.now().strftime("%Y%m%d%H%M%S")
)
# it's a polymorphic object, we need to null both IDs
# https://django-polymorphic.readthedocs.io/en/stable/advanced.html#copying-polymorphic-objects
asset.pk = None
asset.id = None
asset.save()
asset.refresh_from_db()
return asset

def create_download_url(self, asset) -> str:
return build_absolute_uri(reverse("assets-download", args=(asset.pk,)))

Check warning on line 100 in geonode/assets/local.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/local.py#L100

Added line #L100 was not covered by tests

def create_link_url(self, asset) -> str:
return build_absolute_uri(reverse("assets-link", args=(asset.pk,)))

def _is_file_managed(self, file) -> bool:
assets_root = os.path.normpath(settings.ASSETS_ROOT)
return file.startswith(assets_root)

def _are_files_managed(self, 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):
managed = True
else:
unmanaged = True
if managed and unmanaged:
logger.error(f"Both managed and unmanaged files are present: {files}")
raise ValueError("Both managed and unmanaged files are present")

return bool(managed)


class LocalAssetDownloadHandler(AssetDownloadHandlerInterface):

def create_response(self, asset: LocalAsset, attachment: bool = False, basename=None) -> HttpResponse:
if not asset.location:
return HttpResponse("Asset does not contain any data", status=500)

Check warning on line 132 in geonode/assets/local.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/local.py#L132

Added line #L132 was not covered by tests

if len(asset.location) > 1:
logger.warning("TODO: Asset contains more than one file. Download needs to be implemented")

Check warning on line 135 in geonode/assets/local.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/local.py#L135

Added line #L135 was not covered by tests

file0 = asset.location[0]
filename = os.path.basename(file0)
orig_base, ext = os.path.splitext(filename)
outname = f"{basename or orig_base}{ext}"

if _asset_storage_manager.exists(file0):
logger.info(f"Returning file {file0} with name {outname}")

return DownloadResponse(
_asset_storage_manager.open(file0).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)

Check warning on line 152 in geonode/assets/local.py

View check run for this annotation

Codecov / codecov/patch

geonode/assets/local.py#L151-L152

Added lines #L151 - L152 were not covered by tests


asset_handler_registry.register(LocalAssetHandler)
63 changes: 63 additions & 0 deletions geonode/assets/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Generated by Django 4.2.9 on 2024-04-24 10:02

from django.conf import settings
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("base", "0090_alter_resourcebase_polymorphic_ctype"),
]

operations = [
migrations.CreateModel(
name="Asset",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=255)),
("description", models.TextField(blank=True, null=True)),
("type", models.CharField(max_length=255)),
("created", models.DateTimeField(auto_now_add=True)),
("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"polymorphic_ctype",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_%(app_label)s.%(class)s_set+",
to="contenttypes.contenttype",
),
),
],
options={
"verbose_name_plural": "Assets",
},
),
migrations.CreateModel(
name="LocalAsset",
fields=[
(
"asset_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="assets.asset",
),
),
("location", models.JSONField(blank=True, default=list)),
],
options={
"verbose_name_plural": "Local assets",
},
bases=("assets.asset",),
),
]
Empty file.