From d54a95e0f9c689f2bbaaea90a3a16a855a408823 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 17 Aug 2022 13:50:34 +0200 Subject: allow loading packages from zip files through web UI and listing installed mappings --- src/hydrilla/proxy/web_ui/_app.py | 13 ++++ src/hydrilla/proxy/web_ui/packages.py | 89 +++++++++++++++++++++- src/hydrilla/proxy/web_ui/root.py | 19 +++-- .../proxy/web_ui/templates/base.html.jinja | 14 +++- .../proxy/web_ui/templates/packages.html.jinja | 78 +++++++++++++++++++ .../templates/packages__load_from_disk.html.jinja | 14 +++- .../templates/packages__show_single.html.jinja | 32 ++++++++ 7 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 src/hydrilla/proxy/web_ui/_app.py create mode 100644 src/hydrilla/proxy/web_ui/templates/packages.html.jinja create mode 100644 src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja (limited to 'src/hydrilla/proxy/web_ui') diff --git a/src/hydrilla/proxy/web_ui/_app.py b/src/hydrilla/proxy/web_ui/_app.py new file mode 100644 index 0000000..d5783d1 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/_app.py @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: CC0-1.0 + +# Copyright (C) 2022 Wojtek Kosior +# +# Available under the terms of Creative Commons Zero v1.0 Universal. + +import flask + +from .. import state as st + + +class WebUIApp(flask.Flask): + _haketilo_state: st.HaketiloState diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py index 7d67b63..a618ca0 100644 --- a/src/hydrilla/proxy/web_ui/packages.py +++ b/src/hydrilla/proxy/web_ui/packages.py @@ -31,14 +31,99 @@ # Enable using with Python 3.7. from __future__ import annotations +import tempfile +import zipfile import typing as t +from urllib.parse import urlparse +from pathlib import Path + import flask +import werkzeug + +from ...exceptions import HaketiloException +from ...translations import smart_gettext as _ +from .. import state as st +from . import _app + + +class InvalidUploadedMalcontent(HaketiloException): + def __init__(self): + super().__init__(_('err.proxy.uploaded_malcontent_invalid')) bp = flask.Blueprint('load_packages', __package__) -@bp.route('/packages/load_from_disk') -def load_from_disk() -> flask.Response: +@bp.route('/packages/load_from_disk', methods=['GET']) +def load_from_disk_get() -> flask.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: + parsed_url = urlparse(flask.request.referrer) + if parsed_url.netloc != 'hkt.mitm.it': + return load_from_disk_get() + + zip_file_storage = flask.request.files.get('packages_zipfile') + if zip_file_storage is None: + return load_from_disk_get() + + 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 + + state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state + + try: + state.import_packages(malcontent_dir_path) + except: + raise InvalidUploadedMalcontent() + + return flask.redirect(flask.url_for('.packages')) + +@bp.route('/packages') +def packages() -> flask.Response: + state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state + + display_infos = state.mapping_version_store().get_display_infos() + sorted_infos = sorted(display_infos, key=(lambda di: di.info)) + + html = flask.render_template( + 'packages.html.jinja', + display_infos = sorted_infos + ) + return flask.make_response(html, 200) + +@bp.route('/packages/view/') +def show_package(mapping_id: str) -> flask.Response: + state = t.cast(_app.WebUIApp, flask.current_app)._haketilo_state + + try: + store = state.mapping_version_store() + display_info = store.get(mapping_id).get_display_info() + + html = flask.render_template( + 'packages__show_single.html.jinja', + display_info = display_info + ) + return flask.make_response(html, 200) + except st.MissingItemError: + flask.abort(404) diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py index 194251e..64d6be1 100644 --- a/src/hydrilla/proxy/web_ui/root.py +++ b/src/hydrilla/proxy/web_ui/root.py @@ -37,33 +37,36 @@ from threading import Lock import jinja2 import flask -from ...translations import smart_gettext as _ +from ...translations import translation as make_translation +from ... import versions from .. import state as st from .. import http_messages - from . import repos from . import packages +from . import _app -class WebUIApp(flask.Flask): +class WebUIAppImpl(_app.WebUIApp): def __init__(self): super().__init__(__name__) self.jinja_options = { **self.jinja_options, 'loader': jinja2.PackageLoader(__package__), - 'autoescape': jinja2.select_autoescape() + 'autoescape': jinja2.select_autoescape(['html.jinja']), + 'extensions': [ + *self.jinja_options.get('extensions', []), + 'jinja2.ext.i18n' + ] } for blueprint in [repos.bp, packages.bp]: self.register_blueprint(blueprint) - _haketilo_state: st.HaketiloState - # Flask app is not thread-safe and has to be accompanied by an ugly lock. This # can cause slow requests to block other requests, so we might need a better # workaround at some later point. -app = WebUIApp() +app = WebUIAppImpl() app_lock = Lock() @@ -83,6 +86,8 @@ def process_request( with app_lock: app._haketilo_state = state + app.jinja_env.install_gettext_translations(make_translation()) + flask_response = app.test_client().open( path = path, base_url = 'https://hkt.mitm.it', diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja index c6f0dcf..4a9adf8 100644 --- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja @@ -23,7 +23,19 @@ in a proprietary work, I am not going to enforce this in court. {% block head %} - {% block title required %}{% endblock %} - Haketilo proxy + {% block title required %}{% endblock %} - Haketilo proxy + {% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/packages.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja new file mode 100644 index 0000000..d0ba5cb --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/packages.html.jinja @@ -0,0 +1,78 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI package 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 %}Available packages{% endblock %} +{% block style %} +{{ super() }} + +ul#packages_list { + padding: 0; +} + +ul#packages_list > li { + list-style-type: none; + max-width: 100%; + overflow-x: scroll; + white-space: nowrap; + padding: 5px; + margin: 5px; + border: 2px solid #999; + border-radius: 5px; +} + +ul#packages_list > li > a { + display: block; + text-decoration: inherit; + color: inherit; +} + +.package-identifier { + font-size: 80%; + color: #555; +} + +{% endblock %} +{% block main %} +

{{ _('web_ui.h3.packages') }}

+ +{% endblock %} diff --git a/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja index 07ed3b3..52280b2 100644 --- a/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja +++ b/src/hydrilla/proxy/web_ui/templates/packages__load_from_disk.html.jinja @@ -22,5 +22,17 @@ in a proprietary work, I am not going to enforce this in court. {% extends "base.html.jinja" %} {% block title %}Load package{% endblock %} {% block main %} - Not implemented yet :( +
+
+ +
+
+ +
+
+ +
+
{% endblock %} 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 new file mode 100644 index 0000000..5e20dd7 --- /dev/null +++ b/src/hydrilla/proxy/web_ui/templates/packages__show_single.html.jinja @@ -0,0 +1,32 @@ +{# +Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0 + + +Proxy web UI package 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 %} Package details {% endblock %} +{% block style %} +{{ super() }} +{% endblock %} +{% block main %} +

{{ _('web_ui.h3.package_{}').format(display_info.info.long_name) }}

+
+ {{ display_info.info.versioned_identifier }} +
+{% endblock %} -- cgit v1.2.3