diff options
Diffstat (limited to 'src')
17 files changed, 814 insertions, 309 deletions
diff --git a/src/hydrilla/item_infos.py b/src/hydrilla/item_infos.py index 2b89600..ddd1d80 100644 --- a/src/hydrilla/item_infos.py +++ b/src/hydrilla/item_infos.py @@ -38,6 +38,7 @@ if sys.version_info >= (3, 8): else: from typing_extensions import Protocol +import enum import typing as t import dataclasses as dc @@ -51,6 +52,7 @@ from .url_patterns import parse_pattern, ParsedUrl, ParsedPattern from .exceptions import HaketiloException from .translations import smart_gettext as _ + @dc.dataclass(frozen=True, unsafe_hash=True) class ItemSpecifier: """....""" @@ -274,6 +276,21 @@ class ResourceInfo(ItemInfoBase): repo_iteration ) + def __lt__(self, other: 'ResourceInfo') -> bool: + """....""" + return ( + self.identifier, + other.version, + other.revision, + self.repo, + other.repo_iteration + ) < ( + other.identifier, + self.version, + self.revision, + other.repo, + self.repo_iteration + ) def make_payloads(payloads_obj: t.Mapping[str, t.Any]) \ -> t.Mapping[ParsedPattern, ItemSpecifier]: @@ -346,6 +363,31 @@ class MappingInfo(ItemInfoBase): AnyInfo = t.Union[ResourceInfo, MappingInfo] +class ItemType(enum.Enum): + RESOURCE = 'resource' + MAPPING = 'mapping' + + @property + def info_class(self) -> t.Type[AnyInfo]: + if self == ItemType.RESOURCE: + return ResourceInfo + else: + return MappingInfo + + @property + def alt_name(self) -> str: + if self == ItemType.RESOURCE: + return 'library' + else: + return 'package' + + @property + def alt_name_plural(self) -> str: + if self == ItemType.RESOURCE: + return 'libraries' + else: + return 'packages' + LoadedType = t.TypeVar('LoadedType', ResourceInfo, MappingInfo) diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py index 803e727..e8d8b15 100644 --- a/src/hydrilla/proxy/state.py +++ b/src/hydrilla/proxy/state.py @@ -227,10 +227,8 @@ class MappingStore(Store[MappingRef]): def get_display_infos(self) -> t.Sequence[MappingDisplayInfo]: ... - @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class MappingVersionRef(Ref): - """....""" @abstractmethod def install(self) -> None: ... @@ -248,15 +246,49 @@ class MappingVersionStore(Store[MappingVersionRef]): pass +@dc.dataclass(frozen=True) +class ResourceDisplayInfo: + ref: 'ResourceRef' + identifier: str + +@dc.dataclass(frozen=True) +class ResourceVersionDisplayInfo: + ref: 'ResourceVersionRef' + info: item_infos.ResourceInfo + installed: InstalledStatus + active: ActiveStatus + is_orphan: bool + is_local: bool + @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class ResourceRef(Ref): - """....""" - pass + @abstractmethod + def get_version_display_infos(self) \ + -> t.Sequence[ResourceVersionDisplayInfo]: + ... + +class ResourceStore(Store[ResourceRef]): + @abstractmethod + def get_display_infos(self) -> t.Sequence[ResourceDisplayInfo]: + ... @dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc] class ResourceVersionRef(Ref): - """....""" + @abstractmethod + def install(self) -> None: + ... + + @abstractmethod + def uninstall(self) -> t.Optional['ResourceVersionRef']: + ... + + @abstractmethod + def get_all_version_display_infos(self) \ + -> t.Sequence[ResourceVersionDisplayInfo]: + ... + +class ResourceVersionStore(Store[ResourceVersionRef]): pass @@ -360,23 +392,18 @@ class HaketiloState(ABC): @abstractmethod def mapping_store(self) -> MappingStore: - """....""" ... @abstractmethod def mapping_version_store(self) -> MappingVersionStore: - """....""" ... @abstractmethod - def get_resource(self, resource_id: str) -> ResourceRef: - """....""" + def resource_store(self) -> ResourceStore: ... @abstractmethod - def get_resource_version(self, resource_version_id: str) \ - -> ResourceVersionRef: - """....""" + def resource_version_store(self) -> ResourceVersionStore: ... @abstractmethod diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py index f180ec6..6bc6404 100644 --- a/src/hydrilla/proxy/state_impl/concrete_state.py +++ b/src/hydrilla/proxy/state_impl/concrete_state.py @@ -47,7 +47,7 @@ from .. import state as st from .. import policies from .. import simple_dependency_satisfying as sds from . import base -from . import mappings +from . import items from . import repos from . import payloads from . import _operations @@ -61,15 +61,6 @@ class ConcreteRepoIterationRef(st.RepoIterationRef): pass -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteResourceRef(st.ResourceRef): - pass - - -@dc.dataclass(frozen=True, unsafe_hash=True) -class ConcreteResourceVersionRef(st.ResourceVersionRef): - pass - @dc.dataclass class ConcreteHaketiloState(base.HaketiloStateWithFields): def __post_init__(self) -> None: @@ -243,17 +234,16 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields): return ConcreteRepoIterationRef(repo_iteration_id) def mapping_store(self) -> st.MappingStore: - return mappings.ConcreteMappingStore(self) + return items.ConcreteMappingStore(self) def mapping_version_store(self) -> st.MappingVersionStore: - return mappings.ConcreteMappingVersionStore(self) + return items.ConcreteMappingVersionStore(self) - def get_resource(self, resource_id: str) -> st.ResourceRef: - return ConcreteResourceRef(resource_id) + def resource_store(self) -> st.ResourceStore: + return items.ConcreteResourceStore(self) - def get_resource_version(self, resource_version_id: str) \ - -> st.ResourceVersionRef: - return ConcreteResourceVersionRef(resource_version_id) + def resource_version_store(self) -> st.ResourceVersionStore: + return items.ConcreteResourceVersionStore(self) def get_payload(self, payload_id: str) -> st.PayloadRef: raise NotImplementedError() diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/items.py index eb8b4d2..b538dc5 100644 --- a/src/hydrilla/proxy/state_impl/mappings.py +++ b/src/hydrilla/proxy/state_impl/items.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later -# Haketilo proxy data and configuration (MappingRef and MappingStore subtypes). +# Haketilo proxy data and configuration (ResourceStore and MappingStore +# implementations). # # This file is part of Hydrilla&Haketilo. # @@ -25,7 +26,8 @@ # in a proprietary program, I am not going to enforce this in court. """ -This module provides an interface to interact with mappings inside Haketilo. +This module provides an interface to interact with mappings, and resources +inside Haketilo. """ # Enable using with Python 3.7. @@ -39,6 +41,74 @@ from ... import item_infos from .. import state as st from . import base +def _set_installed_status(cursor: sqlite3.Cursor, id: str, new_status: str) \ + -> None: + cursor.execute( + 'UPDATE item_versions SET installed = ? WHERE item_version_id = ?;', + (new_status, id) + ) + +def _get_statuses(cursor: sqlite3.Cursor, id: str) -> t.Tuple[str, str]: + cursor.execute( + ''' + SELECT + installed, active + FROM + item_versions + WHERE + item_version_id = ?; + ''', + (id,) + ) + + rows = cursor.fetchall() + + if rows == []: + raise st.MissingItemError() + + (installed_status, active_status), = rows + + return installed_status, active_status + +VersionRefVar = t.TypeVar( + 'VersionRefVar', + 'ConcreteResourceVersionRef', + 'ConcreteMappingVersionRef' +) + +def _install_version(ref: VersionRefVar) -> None: + with ref.state.cursor(transaction=True) as cursor: + installed_status, _ = _get_statuses(cursor, ref.id) + + if installed_status == 'I': + return + + _set_installed_status(cursor, ref.id, 'I') + + ref.state.pull_missing_files() + +def _uninstall_version(ref: VersionRefVar) -> t.Optional[VersionRefVar]: + with ref.state.cursor(transaction=True) as cursor: + installed_status, active_status = _get_statuses(cursor, ref.id) + + if installed_status == 'N': + return ref + + _set_installed_status(cursor, ref.id, 'N') + + ref.state.prune_orphans() + + if active_status == 'R': + ref.state.recompute_dependencies() + + cursor.execute( + 'SELECT COUNT(*) FROM item_versions WHERE item_version_id = ?;', + (ref.id,) + ) + + (version_still_present,), = cursor.fetchall() + return ref if version_still_present else None + @dc.dataclass(frozen=True, unsafe_hash=True) class ConcreteMappingRef(st.MappingRef): @@ -60,8 +130,8 @@ class ConcreteMappingRef(st.MappingRef): ive.is_local, ms.enabled FROM - item_versions_extra AS ive - JOIN mapping_statuses AS ms USING (item_id) + item_versions_extra AS ive + JOIN mapping_statuses AS ms USING (item_id) WHERE ive.item_id = ?; ''', @@ -111,6 +181,9 @@ class ConcreteMappingStore(st.MappingStore): with self.state.cursor() as cursor: cursor.execute( ''' + WITH available_item_ids AS ( + SELECT DISTINCT item_id FROM item_versions + ) SELECT i.item_id, i.identifier, @@ -128,8 +201,9 @@ class ConcreteMappingStore(st.MappingStore): JOIN mapping_statuses AS ms USING (item_id) LEFT JOIN item_versions_extra AS ive - ON ms.active_version_id = ive.item_version_id AND - ive.active IN ('R', 'A'); + ON ms.active_version_id = ive.item_version_id + WHERE + i.item_id IN available_item_ids; ''' ) @@ -181,79 +255,142 @@ class ConcreteMappingStore(st.MappingStore): @dc.dataclass(frozen=True, unsafe_hash=True) class ConcreteMappingVersionRef(st.MappingVersionRef): - state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) + state: base.HaketiloStateWithFields - def _set_installed_status(self, cursor: sqlite3.Cursor, new_status: str) \ - -> None: - cursor.execute( - ''' - UPDATE - item_versions - SET - installed = ? - WHERE - item_version_id = ?; - ''', - (new_status, self.id,) - ) + def install(self) -> None: + return _install_version(self) - def _get_statuses(self, cursor: sqlite3.Cursor) -> t.Tuple[str, str]: - cursor.execute( - ''' - SELECT - installed, active - FROM - item_versions - WHERE - item_version_id = ?; - ''', - (self.id,) - ) + def uninstall(self) -> t.Optional['ConcreteMappingVersionRef']: + return _uninstall_version(self) - rows = cursor.fetchall() + def get_all_version_display_infos(self) \ + -> t.Sequence[st.MappingVersionDisplayInfo]: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT + item_id + FROM + item_versions + WHERE + item_version_id = ?; + ''', + (self.id,) + ) - if rows == []: - raise st.MissingItemError() + rows = cursor.fetchall() + if rows == []: + raise st.MissingItemError() - (installed_status, active_status), = rows + (mapping_id,), = rows - return installed_status, active_status + mapping_ref = ConcreteMappingRef(str(mapping_id), self.state) - def install(self) -> None: - with self.state.cursor(transaction=True) as cursor: - installed_status, _ = self._get_statuses(cursor) + return mapping_ref.get_version_display_infos() + + +@dc.dataclass(frozen=True) +class ConcreteMappingVersionStore(st.MappingVersionStore): + state: base.HaketiloStateWithFields + + def get(self, id: str) -> st.MappingVersionRef: + return ConcreteMappingVersionRef(str(int(id)), self.state) + + +@dc.dataclass(frozen=True, unsafe_hash=True) +class ConcreteResourceRef(st.ResourceRef): + state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False) - if installed_status == 'I': - return + def get_version_display_infos(self) \ + -> t.Sequence[st.ResourceVersionDisplayInfo]: + with self.state.cursor() as cursor: + cursor.execute( + ''' + SELECT + ive.item_version_id, + ive.definition, + ive.repo, + ive.repo_iteration, + ive.installed, + ive.active, + ive.is_orphan, + ive.is_local + FROM + item_versions_extra AS ive + JOIN items AS i USING (item_id) + WHERE + i.type = 'R' AND ive.item_id = ?; + ''', + (self.id,) + ) - self._set_installed_status(cursor, 'I') + rows = cursor.fetchall() - self.state.pull_missing_files() + if rows == []: + raise st.MissingItemError() - def uninstall(self) -> None: - with self.state.cursor(transaction=True) as cursor: - installed_status, active_status = self._get_statuses(cursor) + result = [] - if installed_status == 'N': - return + for (item_version_id, definition, repo, repo_iteration, + installed_status, active_status, is_orphan, is_local) in rows: + ref = ConcreteResourceVersionRef(str(item_version_id), self.state) - self._set_installed_status(cursor, 'N') + item_info = item_infos.ResourceInfo.load( + definition, + repo, + repo_iteration + ) - self.state.prune_orphans() + display_info = st.ResourceVersionDisplayInfo( + ref = ref, + info = item_info, + installed = st.InstalledStatus(installed_status), + active = st.ActiveStatus(active_status), + is_orphan = is_orphan, + is_local = is_local + ) + result.append(display_info) + + return sorted(result, key=(lambda di: di.info)) + + +@dc.dataclass(frozen=True) +class ConcreteResourceStore(st.ResourceStore): + state: base.HaketiloStateWithFields - if active_status == 'R': - self.state.recompute_dependencies() + def get(self, id: str) -> st.ResourceRef: + return ConcreteResourceRef(str(int(id)), self.state) + def get_display_infos(self) -> t.Sequence[st.ResourceDisplayInfo]: + with self.state.cursor() as cursor: cursor.execute( - 'SELECT COUNT(*) FROM item_versions WHERE item_version_id = ?;', - (self.id,) + "SELECT item_id, identifier FROM items WHERE type = 'R';" ) - (version_still_present,), = cursor.fetchall() - return self if version_still_present else None + rows = cursor.fetchall() + + result = [] + + for item_id, identifier in rows: + ref = ConcreteResourceRef(str(item_id), self.state) + + result.append(st.ResourceDisplayInfo(ref, identifier)) + + return sorted(result, key=(lambda di: di.identifier)) + + +@dc.dataclass(frozen=True, unsafe_hash=True) +class ConcreteResourceVersionRef(st.ResourceVersionRef): + state: base.HaketiloStateWithFields + + def install(self) -> None: + return _install_version(self) + + def uninstall(self) -> t.Optional['ConcreteResourceVersionRef']: + return _uninstall_version(self) def get_all_version_display_infos(self) \ - -> t.Sequence[st.MappingVersionDisplayInfo]: + -> t.Sequence[st.ResourceVersionDisplayInfo]: with self.state.cursor() as cursor: cursor.execute( ''' @@ -271,16 +408,16 @@ class ConcreteMappingVersionRef(st.MappingVersionRef): if rows == []: raise st.MissingItemError() - (mapping_id,), = rows + (resource_id,), = rows - mapping_ref = ConcreteMappingRef(str(mapping_id), self.state) + resource_ref = ConcreteResourceRef(str(resource_id), self.state) - return mapping_ref.get_version_display_infos() + return resource_ref.get_version_display_infos() @dc.dataclass(frozen=True) -class ConcreteMappingVersionStore(st.MappingVersionStore): +class ConcreteResourceVersionStore(st.ResourceVersionStore): state: base.HaketiloStateWithFields - def get(self, id: str) -> st.MappingVersionRef: - return ConcreteMappingVersionRef(str(int(id)), self.state) + def get(self, id: str) -> st.ResourceVersionRef: + return ConcreteResourceVersionRef(str(int(id)), self.state) diff --git a/src/hydrilla/proxy/web_ui/items.py b/src/hydrilla/proxy/web_ui/items.py new file mode 100644 index 0000000..03f2f2d --- /dev/null +++ b/src/hydrilla/proxy/web_ui/items.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Proxy web UI packages loading. +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# 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 <https://www.gnu.org/licenses/>. +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use this code +# in a proprietary program, I am not going to enforce this in court. + +""" +..... +""" + +# Enable using with Python 3.7. +from __future__ import annotations + +import tempfile +import zipfile +import typing as t + +from pathlib import Path + +import flask +import werkzeug + +from ...exceptions import HaketiloException +from ...translations import smart_gettext as _ +from ... import item_infos +from .. import state as st +from . import _app + + +class InvalidUploadedMalcontent(HaketiloException): + def __init__(self): + super().__init__(_('err.proxy.uploaded_malcontent_invalid')) + + +bp = flask.Blueprint('items', __package__) + +@bp.route('/packages/load_from_disk', methods=['GET']) +def load_from_disk() -> werkzeug.Response: + html = flask.render_template('packages/load_from_disk.html.jinja') + return flask.make_response(html, 200) + +@bp.route('/packages/load_from_disk', methods=['POST']) +def load_from_disk_post() -> werkzeug.Response: + zip_file_storage = flask.request.files.get('packages_zipfile') + if zip_file_storage is None: + return load_from_disk() + + with tempfile.TemporaryDirectory() as tmpdir_str: + tmpdir = Path(tmpdir_str) + tmpdir_child = tmpdir / 'childdir' + tmpdir_child.mkdir() + + try: + with zipfile.ZipFile(zip_file_storage) as zip_file: + zip_file.extractall(tmpdir_child) + except: + raise HaketiloException(_('err.proxy.uploaded_file_not_zip')) + + extracted_top_level_files = tuple(tmpdir_child.iterdir()) + if extracted_top_level_files == (): + raise InvalidUploadedMalcontent() + + if len(extracted_top_level_files) == 1 and \ + extracted_top_level_files[0].is_dir(): + malcontent_dir_path = extracted_top_level_files[0] + else: + malcontent_dir_path = tmpdir_child + + try: + _app.get_haketilo_state().import_items(malcontent_dir_path) + except: + raise InvalidUploadedMalcontent() + + return flask.redirect(flask.url_for('.packages')) + +@bp.route('/packages') +def packages() -> werkzeug.Response: + store = _app.get_haketilo_state().mapping_store() + + html = flask.render_template( + 'packages/index.html.jinja', + display_infos = store.get_display_infos() + ) + return flask.make_response(html, 200) + +@bp.route('/libraries') +def libraries() -> werkzeug.Response: + store = _app.get_haketilo_state().resource_store() + + html = flask.render_template( + 'libraries/index.html.jinja', + display_infos = store.get_display_infos() + ) + return flask.make_response(html, 200) + +def item_store(state: st.HaketiloState, item_type: item_infos.ItemType) \ + -> t.Union[st.MappingStore, st.ResourceStore]: + if item_type == item_infos.ItemType.RESOURCE: + return state.resource_store() + else: + return state.mapping_store() + +def show_item(item_id: str, item_type: item_infos.ItemType) \ + -> werkzeug.Response: + try: + store = item_store(_app.get_haketilo_state(), item_type) + item_ref = store.get(str(item_id)) + version_display_infos = item_ref.get_version_display_infos() + + display_info: t.Union[st.MappingDisplayInfo, st.ResourceDisplayInfo] + + if isinstance(item_ref, st.ResourceRef): + display_info = st.ResourceDisplayInfo( + ref = item_ref, + identifier = version_display_infos[0].info.identifier + ) + else: + version_display_infos = t.cast( + t.Sequence[st.MappingVersionDisplayInfo], + version_display_infos + ) + + active_version: t.Optional[st.MappingVersionDisplayInfo] = None + + for info in version_display_infos: + if info.active != st.ActiveStatus.NOT_ACTIVE: + active_version = info + + display_info = st.MappingDisplayInfo( + ref = item_ref, + identifier = version_display_infos[0].info.identifier, + enabled = version_display_infos[0].mapping_enabled, + active_version = active_version + ) + + html = flask.render_template( + f'{item_type.alt_name_plural}/show_single.html.jinja', + display_info = display_info, + version_display_infos = version_display_infos + ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) + + +@bp.route('/libraries/view/<string:item_id>') +def show_library(item_id: str) -> werkzeug.Response: + return show_item(item_id, item_infos.ItemType.RESOURCE) + +@bp.route('/packages/view/<string:item_id>') +def show_package(item_id: str) -> werkzeug.Response: + return show_item(item_id, item_infos.ItemType.MAPPING) + +ItemVersionDisplayInfo = t.Union[ + st.MappingVersionDisplayInfo, + st.ResourceVersionDisplayInfo +] + +def item_version_store( + state: st.HaketiloState, + item_type: item_infos.ItemType +) -> t.Union[st.MappingVersionStore, st.ResourceVersionStore]: + if item_type == item_infos.ItemType.RESOURCE: + return state.resource_version_store() + else: + return state.mapping_version_store() + +def show_item_version( + item_version_id: str, + item_type: item_infos.ItemType, + errors: t.Mapping[str, bool] = {} +) -> werkzeug.Response: + try: + store = item_version_store(_app.get_haketilo_state(), item_type) + version_ref = store.get(item_version_id) + display_infos = version_ref.get_all_version_display_infos() + + other_infos: list[ItemVersionDisplayInfo] = [] + this_info: t.Optional[ItemVersionDisplayInfo] = None + + for info in display_infos: + if info.ref == version_ref: + this_info = info + else: + other_infos.append(info) + + assert this_info is not None + + html = flask.render_template( + f'{item_type.alt_name_plural}/show_single_version.html.jinja', + display_info = this_info, + version_display_infos = other_infos, + **errors + ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) + +@bp.route('/libraries/viewversion/<string:item_version_id>') +def show_library_version(item_version_id: str) -> werkzeug.Response: + return show_item_version(item_version_id, item_infos.ItemType.RESOURCE) + +@bp.route('/packages/viewversion/<string:item_version_id>') +def show_package_version(item_version_id: str) -> werkzeug.Response: + return show_item_version(item_version_id, item_infos.ItemType.MAPPING) + +def alter_item_version(item_version_id: str, item_type: item_infos.ItemType) \ + -> werkzeug.Response: + form_data = flask.request.form + action = form_data['action'] + + try: + store = item_version_store(_app.get_haketilo_state(), item_type) + item_version_ref = store.get(item_version_id) + + if action == 'install_item_version': + item_version_ref.install() + elif action == 'uninstall_item_version': + item_version_ref_after = item_version_ref.uninstall() + if item_version_ref_after is None: + url = flask.url_for(f'.{item_type.alt_name_plural}') + return flask.redirect(url) + else: + return show_item_version(item_version_id, item_type) + else: + raise ValueError() + except st.FileInstallationError: + return show_item_version( + item_version_id = item_version_id, + item_type = item_type, + errors = {'file_installation_error': True} + ) + except st.ImpossibleSituation: + return show_item_version( + item_version_id = item_version_id, + item_type = item_type, + errors = {'uninstall_disallowed': True} + ) + except st.MissingItemError: + flask.abort(404) + + return flask.redirect( + flask.url_for( + f'.show_{item_type.alt_name}_version', + item_version_id = item_version_id + ) + ) + +@bp.route('/libraries/viewversion/<string:item_version_id>', methods=['POST']) +def alter_library_version(item_version_id: str) -> werkzeug.Response: + return alter_item_version(item_version_id, item_infos.ItemType.RESOURCE) + +@bp.route('/packages/viewversion/<string:item_version_id>', methods=['POST']) +def alter_package_version(item_version_id: str) -> werkzeug.Response: + return alter_item_version(item_version_id, item_infos.ItemType.MAPPING) diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py deleted file mode 100644 index 31d3dbb..0000000 --- a/src/hydrilla/proxy/web_ui/packages.py +++ /dev/null @@ -1,203 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later - -# Proxy web UI packages loading. -# -# This file is part of Hydrilla&Haketilo. -# -# Copyright (C) 2022 Wojtek Kosior -# -# 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 <https://www.gnu.org/licenses/>. -# -# -# I, Wojtek Kosior, thereby promise not to sue for violation of this -# file's license. Although I request that you do not make use this code -# in a proprietary program, I am not going to enforce this in court. - -""" -..... -""" - -# Enable using with Python 3.7. -from __future__ import annotations - -import tempfile -import zipfile -import typing as t - -from pathlib import Path - -import flask -import werkzeug - -from ...exceptions import HaketiloException -from ...translations import smart_gettext as _ -from ... import item_infos -from .. import state as st -from . import _app - - -class InvalidUploadedMalcontent(HaketiloException): - def __init__(self): - super().__init__(_('err.proxy.uploaded_malcontent_invalid')) - - -bp = flask.Blueprint('packages', __package__) - -@bp.route('/packages/load_from_disk', methods=['GET']) -def load_from_disk() -> werkzeug.Response: - html = flask.render_template('packages/load_from_disk.html.jinja') - return flask.make_response(html, 200) - -@bp.route('/packages/load_from_disk', methods=['POST']) -def load_from_disk_post() -> werkzeug.Response: - zip_file_storage = flask.request.files.get('packages_zipfile') - if zip_file_storage is None: - return load_from_disk() - - with tempfile.TemporaryDirectory() as tmpdir_str: - tmpdir = Path(tmpdir_str) - tmpdir_child = tmpdir / 'childdir' - tmpdir_child.mkdir() - - try: - with zipfile.ZipFile(zip_file_storage) as zip_file: - zip_file.extractall(tmpdir_child) - except: - raise HaketiloException(_('err.proxy.uploaded_file_not_zip')) - - extracted_top_level_files = tuple(tmpdir_child.iterdir()) - if extracted_top_level_files == (): - raise InvalidUploadedMalcontent() - - if len(extracted_top_level_files) == 1 and \ - extracted_top_level_files[0].is_dir(): - malcontent_dir_path = extracted_top_level_files[0] - else: - malcontent_dir_path = tmpdir_child - - try: - _app.get_haketilo_state().import_items(malcontent_dir_path) - except: - raise InvalidUploadedMalcontent() - - return flask.redirect(flask.url_for('.packages')) - -@bp.route('/packages') -def packages() -> werkzeug.Response: - store = _app.get_haketilo_state().mapping_store() - - html = flask.render_template( - 'packages/index.html.jinja', - display_infos = store.get_display_infos() - ) - return flask.make_response(html, 200) - -@bp.route('/packages/view/<string:mapping_id>') -def show_package(mapping_id: str) -> werkzeug.Response: - try: - store = _app.get_haketilo_state().mapping_store() - mapping_ref = store.get(str(mapping_id)) - version_display_infos = mapping_ref.get_version_display_infos() - - active_version: t.Optional[st.MappingVersionDisplayInfo] = None - - for info in version_display_infos: - if info.active != st.ActiveStatus.NOT_ACTIVE: - active_version = info - - display_info = st.MappingDisplayInfo( - ref = mapping_ref, - identifier = version_display_infos[0].info.identifier, - enabled = version_display_infos[0].mapping_enabled, - active_version = active_version - ) - - html = flask.render_template( - 'packages/show_single.html.jinja', - display_info = display_info, - version_display_infos = version_display_infos - ) - return flask.make_response(html, 200) - except st.MissingItemError: - flask.abort(404) - -@bp.route('/packages/viewversion/<string:mapping_version_id>') -def show_package_version( - mapping_version_id: str, - errors: t.Mapping[str, bool] = {} -) -> werkzeug.Response: - try: - store = _app.get_haketilo_state().mapping_version_store() - version_ref = store.get(mapping_version_id) - display_infos = version_ref.get_all_version_display_infos() - - other_infos: list[st.MappingVersionDisplayInfo] = [] - this_info: t.Optional[st.MappingVersionDisplayInfo] = None - - for info in display_infos: - if info.ref == version_ref: - this_info = info - else: - other_infos.append(info) - - assert this_info is not None - - html = flask.render_template( - 'packages/show_single_version.html.jinja', - display_info = this_info, - version_display_infos = other_infos, - **errors - ) - return flask.make_response(html, 200) - except st.MissingItemError: - flask.abort(404) - -@bp.route('/packages/viewversion/<string:mapping_version_id>', methods=['POST']) -def alter_package_version(mapping_version_id: str) -> werkzeug.Response: - form_data = flask.request.form - action = form_data['action'] - - try: - store = _app.get_haketilo_state().mapping_version_store() - mapping_version_ref = store.get(mapping_version_id) - - if action == 'install_package': - mapping_version_ref.install() - elif action == 'uninstall_package': - mapping_version_ref = mapping_version_ref.uninstall() - if mapping_version_ref is None: - return flask.redirect(flask.url_for('.packages')) - else: - return show_package_version(mapping_version_id) - else: - raise ValueError() - except st.FileInstallationError: - return show_package_version( - mapping_version_id, - {'file_installation_error': True} - ) - except st.ImpossibleSituation: - return show_package_version( - mapping_version_id, - {'uninstall_disallowed': True} - ) - except st.MissingItemError: - flask.abort(404) - - return flask.redirect( - flask.url_for( - '.show_package_version', - mapping_version_id = mapping_version_id - ) - ) diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index 28d7262..ab4e09b 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -45,7 +45,7 @@ from ... import versions from .. import state as st from .. import http_messages from . import repos -from . import packages +from . import items from . import _app @@ -90,7 +90,7 @@ class WebUIAppImpl(_app.WebUIApp): self.before_request(authenticate_by_referrer) - for blueprint in [repos.bp, packages.bp]: + for blueprint in [repos.bp, items.bp]: self.register_blueprint(blueprint) # Flask app is not thread-safe and has to be accompanied by an ugly lock. This diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index 4d1eca2..acc696e 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -119,10 +119,11 @@ in a proprietary work, I am not going to enforce this in court. {% set active_endpoint = get_current_endpoint() %} {% set navigation_bar = [ - ('home', _('web_ui.base.nav.home')), - ('packages.packages', _('web_ui.base.nav.packages')), - ('repos.repos', _('web_ui.base.nav.repos')), - ('packages.load_from_disk', _('web_ui.base.nav.load')) + ('home', _('web_ui.base.nav.home')), + ('items.packages', _('web_ui.base.nav.packages')), + ('items.libraries', _('web_ui.base.nav.libraries')), + ('repos.repos', _('web_ui.base.nav.repos')), + ('items.load_from_disk', _('web_ui.base.nav.load')) ] %} <ul id="nav"> diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja new file mode 100644 index 0000000..5cdda04 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja @@ -0,0 +1,39 @@ +{# +Proxy web UI library list page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} +{% block title %} {{ _('web_ui.libraries.title') }} {% endblock %} +{% block style %} + {{ super() }} + + {% include 'include/item_list_style.css.jinja' %} +{% endblock %} +{% block main %} + <h3>{{ _('web_ui.libraries.heading') }}</h3> + <ul id="item_list"> + {% for info in display_infos %} + <li> + <a href="{{ url_for('.show_library', item_id=info.ref.id) }}"> + <div> + {{ info.identifier }} + </div> + </a> + </li> + {% endfor %} + </ul> +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license new file mode 100644 index 0000000..bb2e0af --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license @@ -0,0 +1,7 @@ +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Copyright (C) 2022 Wojtek Kosior + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja new file mode 100644 index 0000000..8ee96ba --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja @@ -0,0 +1,76 @@ +{# +Proxy web UI library show page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "base.html.jinja" %} +{% block title %} {{ _('web_ui.libraries.single.title') }} {% endblock %} +{% block style %} + {{ super() }} + + {% include 'include/item_list_style.css.jinja' %} +{% endblock %} +{% block main %} + {% block main_info %} + <h3> + {{ + _('web_ui.libraries.single.heading.name_{}') + .format(display_info.identifier) + }} + </h3> + TODO: add more info... + {% endblock %} + <h4> + {% if version_display_infos|length > 0 %} + {% block version_list_heading %} + {{ _('web_ui.libraries.single.version_list_heading') }} + {% endblock %} + {% endif %} + </h4> + <ul id="item_list"> + {% for info in version_display_infos %} + {% set entry_classes = [] %} + {% if info.is_local %} + {% do entry_classes.append('version-entry-local') %} + {% endif %} + {% if info.is_orphan %} + {% do entry_classes.append('version-entry-orphan') %} + {% endif %} + <li class="{{ entry_classes|join(' ') }}"> + {% + set href = url_for( + '.show_library_version', + item_version_id = info.ref.id + ) + %} + <a href="{{ href }}"> + <div> + {{ + versions.version_string( + info.info.version, + rev = info.info.revision + ) + }} + {% if not info.is_local %} + @ + {{ info.info.repo }} + {% endif %} + </div> + </a> + </li> + {% endfor %} + </ul> +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license new file mode 100644 index 0000000..bb2e0af --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license @@ -0,0 +1,7 @@ +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Copyright (C) 2022 Wojtek Kosior + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja new file mode 100644 index 0000000..448a9bc --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja @@ -0,0 +1,94 @@ +{# +Proxy web UI library version show page. + +This file is part of Hydrilla&Haketilo. + +Copyright (C) 2022 Wojtek Kosior + +Dual licensed under +* GNU General Public License v3.0 or later and +* Creative Commons Attribution Share Alike 4.0 International. + +You can choose to use either of these licenses or both. + + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. +#} +{% extends "libraries/show_single.html.jinja" %} +{% block title %} {{ _('web_ui.libraries.single_version.title') }} {% endblock %} +{% block main_info %} + {% if file_installation_error is defined %} + <aside class="error-note"> + {{ _('web_ui.err.file_installation_error') }} + </aside> + {% endif %} + {% if uninstall_disallowed is defined %} + <aside class="error-note"> + {{ _('web_ui.err.uninstall_disallowed') }} + </aside> + {% endif %} + {% if repo_communication_error is defined %} + <aside class="error-note"> + {{ _('web_ui.err.repo_communication_error') }} + </aside> + {% endif %} + <h3> + {{ + _('web_ui.libraries.single_version.heading.name_{}') + .format(display_info.info.long_name) + }} + </h3> + <div class="library-identifier"> + {{ display_info.info.versioned_identifier }} + </div> + TODO: add more info... + {% if display_info.installed == InstalledStatus.INSTALLED %} + <div> + {{ _('web_ui.libraries.single_version.library_is_installed') }} + </div> + {% + if uninstall_disallowed is not defined and + display_info.active != ActiveStatus.REQUIRED + %} + <form method="POST"> + <input name="action" value="uninstall_item_version" type="hidden"> + <button class="green-button"> + {{ _('web_ui.libraries.single_version.uninstall_button') }} + </button> + </form> + {% endif %} + {% elif display_info.installed == InstalledStatus.NOT_INSTALLED %} + <div> + {{ _('web_ui.libraries.single_version.library_is_not_installed') }} + </div> + <form method="POST"> + <input name="action" value="install_item_version" type="hidden"> + <button class="green-button"> + {{ _('web_ui.libraries.single_version.install_button') }} + </button> + </form> + {% else %} + <div> + {{ _('web_ui.libraries.single_version.library_install_failed') }} + </div> + <div> + <form method="POST" class="inline-form"> + <input name="action" value="install_item_version" type="hidden"> + <button class="green-button"> + {{ _('web_ui.libraries.single_version.retry_install_button') }} + </button> + </form> + <form method="POST" class="inline-form"> + <input name="action" value="uninstall_item_version" type="hidden"> + <button class="green-button"> + {{ _('web_ui.libraries.single_version.leave_uninstalled_button') }} + </button> + </form> + </div> + {% endif %} +{% endblock main_info %} +{% block version_list_heading %} + {{ _('web_ui.libraries.single_version.version_list_heading') }} +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license new file mode 100644 index 0000000..bb2e0af --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license @@ -0,0 +1,7 @@ +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + +Copyright (C) 2022 Wojtek Kosior + +I, Wojtek Kosior, thereby promise not to sue for violation of this +file's licenses. Although I request that you do not make use this code +in a proprietary work, I am not going to enforce this in court. diff --git a/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja index 6aa985c..e2aad5d 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja @@ -27,8 +27,14 @@ in a proprietary work, I am not going to enforce this in court. <h3>{{ _('web_ui.packages.heading') }}</h3> <ul id="item_list"> {% for info in display_infos %} - <li class="{{ entry_classes }}"> - <a href="{{ url_for('.show_package', mapping_id=info.ref.id) }}"> + {% set entry_classes = [] %} + {% if info.enabled == EnabledStatus.ENABLED %} + {% do entry_classes.append('mapping-entry-enabled') %} + {% elif info.enabled == EnabledStatus.DISABLED %} + {% do entry_classes.append('mapping-entry-disabled') %} + {% endif %} + <li class="{{ entry_classes|join(' ') }}"> + <a href="{{ url_for('.show_package', item_id=info.ref.id) }}"> <div> {{ info.identifier }} </div> diff --git a/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja index 60cb4a5..24d9a58 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja @@ -53,7 +53,7 @@ in a proprietary work, I am not going to enforce this in court. {% set href = url_for( '.show_package_version', - mapping_version_id = info.ref.id + item_version_id = info.ref.id ) %} <a href="{{ href }}"> diff --git a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja index 2c6863b..12e5416 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja @@ -53,7 +53,7 @@ in a proprietary work, I am not going to enforce this in court. display_info.active != ActiveStatus.REQUIRED %} <form method="POST"> - <input name="action" value="uninstall_package" type="hidden"> + <input name="action" value="uninstall_item_version" type="hidden"> <button class="green-button"> {{ _('web_ui.packages.single_version.uninstall_button') }} </button> @@ -64,7 +64,7 @@ in a proprietary work, I am not going to enforce this in court. {{ _('web_ui.packages.single_version.package_is_not_installed') }} </div> <form method="POST"> - <input name="action" value="install_package" type="hidden"> + <input name="action" value="install_item_version" type="hidden"> <button class="green-button"> {{ _('web_ui.packages.single_version.install_button') }} </button> @@ -75,13 +75,13 @@ in a proprietary work, I am not going to enforce this in court. </div> <div> <form method="POST" class="inline-form"> - <input name="action" value="install_package" type="hidden"> + <input name="action" value="install_item_version" type="hidden"> <button class="green-button"> {{ _('web_ui.packages.single_version.retry_install_button') }} </button> </form> <form method="POST" class="inline-form"> - <input name="action" value="uninstall_package" type="hidden"> + <input name="action" value="uninstall_item_version" type="hidden"> <button class="green-button"> {{ _('web_ui.packages.single_version.leave_uninstalled_button') }} </button> |