Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion ckanext/unfold/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def get_archive_structure(
)

try:
nodes = unf_utils.get_archive_tree(resource, resource_view)
nodes = unf_utils.get_archive_tree(resource, resource_view, context)
except unf_exception.UnfoldError as e:
return {"error": str(e)}

Expand Down
2 changes: 1 addition & 1 deletion ckanext/unfold/templates/unfold_preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
data-module-resource-id="{{ resource.id }}"
data-module-resource-view-id="{{ resource_view.id }}"
data-module-resource-url="{{ resource.url }}"
data-module-resource-remote="{{ resource.url_type != 'upload' }}"
data-module-resource-remote="{{ resource.url_type not in ('upload', 'file') }}"
data-module-resource-format="{{ resource.format }}"
data-module-show-context-menu="{{ (show_context_menu_default if resource_view.show_context_menu is undefined else resource_view.show_context_menu) | tojson }}">
<div id="archive-tree--loader" class="ms-4">
Expand Down
27 changes: 27 additions & 0 deletions ckanext/unfold/tests/test_unfold.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
from contextlib import contextmanager
from typing import Iterator

import pytest

Expand Down Expand Up @@ -48,3 +50,28 @@ def test_build_complex_tree():
assert len(tree) == 15004
root_folders = [node for node in tree if node.parent == "#"]
assert len(root_folders) == 4


@pytest.mark.usefixtures("with_request_context")
def test_build_file_resource_tree(monkeypatch, ckan_config):
file_path = os.path.join(os.path.dirname(__file__), "data/test_archive.zip")
resource = {
"id": "file-resource",
"format": "zip",
"url_type": "file",
}
ckan_config["ckanext.unfold.enable_cache"] = False

@contextmanager
def prepare_file_resource(
resource: dict,
context: dict,
) -> Iterator[tuple[dict, str]]:
yield resource, file_path

monkeypatch.setattr(utils, "_prepare_file_resource", prepare_file_resource)

tree = utils.get_archive_tree(resource, {})

assert len(tree) == 11
assert isinstance(tree[0], types.Node)
98 changes: 95 additions & 3 deletions ckanext/unfold/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import math
import pathlib
import mimetypes
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import asdict
from typing import Any

Expand All @@ -22,6 +25,7 @@

DEFAULT_DATE_FORMAT = "%d/%m/%Y - %H:%M"
REDIS_CACHE_TTL = 3600 * 24 # 24 hour
TEMPORARY_LINK_TTL = 300
log = logging.getLogger(__name__)


Expand Down Expand Up @@ -173,7 +177,9 @@ def close(cls) -> None:


def get_archive_tree(
resource: dict[str, Any], resource_view: dict[str, Any]
resource: dict[str, Any],
resource_view: dict[str, Any],
context: dict[str, Any] | None = None,
) -> list[unf_types.Node]:
cache_enabled = unf_config.is_cache_enabled()
cached_tree = UnfoldCacheManager.get(resource["id"])
Expand All @@ -189,15 +195,101 @@ def get_archive_tree(
if "cloudstorage" in tk.g.plugins:
_prepare_cloudstorage_resource(resource)

adapter_instance = adapter_cls(resource, resource_view)
archive_tree = adapter_instance.build_archive_tree()
if resource.get("url_type") == "file":
with _prepare_file_resource(resource, context or {}) as prepared:
archive_tree = _build_archive_tree(adapter_cls, resource_view, *prepared)
else:
archive_tree = _build_archive_tree(adapter_cls, resource_view, resource)

if cache_enabled:
UnfoldCacheManager.save(archive_tree, resource["id"])

return archive_tree


def _build_archive_tree(
adapter_cls: type[unf_adapters.BaseAdapter],
resource_view: dict[str, Any],
resource: dict[str, Any],
filepath: str | None = None,
) -> list[unf_types.Node]:
adapter_instance = adapter_cls(resource, resource_view, filepath=filepath)
return adapter_instance.build_archive_tree()


@contextmanager
def _prepare_file_resource(
resource: dict[str, Any],
context: dict[str, Any],
) -> Iterator[tuple[dict[str, Any], str | None]]:
"""Make a ckanext-files resource readable by an archive adapter."""
file_id = resource.get("url", "").rstrip("/").rsplit("/", 1)[-1]
if not file_id:
raise unf_exception.UnfoldError("Unable to determine the resource file")

try:
files = _get_files_api()
file_info = tk.get_action("files_file_show")(context, {"id": file_id})
storage = files.get_storage(file_info["storage"])
file_data = files.FileData.from_dict(file_info)
except Exception as error:
raise unf_exception.UnfoldError(
f"Unable to access the resource file: {error}"
) from error

try:
temporary_url = storage.temporary_link(
file_data,
TEMPORARY_LINK_TTL,
)
except Exception:
log.exception("Unable to create a temporary archive link")
temporary_url = None

if temporary_url:
adapter_resource = resource.copy()
adapter_resource.update(
{
"url": temporary_url,
"type": "url",
"size": file_info.get("size", resource.get("size")),
}
)
yield adapter_resource, None
return

if not storage.supports(files.Capability.STREAM):
raise unf_exception.UnfoldError("Resource storage does not support reading files")

suffix = pathlib.Path(file_info.get("name", "")).suffix
with tempfile.NamedTemporaryFile(suffix=suffix) as target:
try:
for chunk in storage.stream(file_data):
target.write(chunk)
target.flush()
except Exception as error:
raise unf_exception.UnfoldError(
f"Unable to read the resource file: {error}"
) from error

yield resource, target.name


def _get_files_api() -> Any:
"""Load the storage API only when a ckanext-files resource is used."""
try:
from ckan.lib import files
except ImportError:
try:
from ckanext.files import shared as files
except ImportError as error:
raise unf_exception.UnfoldError(
"ckanext-files is required to read this resource"
) from error

return files


def _prepare_cloudstorage_resource(resource: dict[str, Any]) -> None:
uploader = get_resource_uploader(resource)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ckanext-unfold"
version = "2.3.3"
version = "2.4.0"
description = "Provides previews for multiple archive formats"
authors = [
{name = "DataShades", email = "datashades@linkdigital.com.au"},
Expand Down
Loading